From 5db100468fd50ccb69edc26223c870b0935fb133 Mon Sep 17 00:00:00 2001 From: Maia Iyer Date: Mon, 30 Sep 2024 12:46:38 -0400 Subject: [PATCH 1/4] separate spire and tornjak APIs Signed-off-by: Maia Iyer --- api/agent/{api.go => spire_apis.go} | 121 --------------------------- api/agent/tornjak_apis.go | 125 ++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 121 deletions(-) rename api/agent/{api.go => spire_apis.go} (80%) create mode 100644 api/agent/tornjak_apis.go diff --git a/api/agent/api.go b/api/agent/spire_apis.go similarity index 80% rename from api/agent/api.go rename to api/agent/spire_apis.go index f3bcf74d..fc84e8be 100644 --- a/api/agent/api.go +++ b/api/agent/spire_apis.go @@ -14,8 +14,6 @@ import ( 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" - - tornjakTypes "github.com/spiffe/tornjak/pkg/agent/types" ) type HealthcheckRequest grpc_health_v1.HealthCheckRequest @@ -407,122 +405,3 @@ func (s *Server) DeleteFederationRelationship(inp DeleteFederationRelationshipRe return (*DeleteFederationRelationshipResponse)(bundle), nil } - - -/* - -Agent - -ListAgents(ListAgentsRequest) returns (ListAgentsResponse); -BanAgent(BanAgentRequest) returns (google.protobuf.Empty); -DeleteAgent(DeleteAgentRequest) returns (google.protobuf.Empty); -CreateJoinToken(CreateJoinTokenRequest) returns (spire.types.JoinToken); - -Entries - -ListEntries(ListEntriesRequest) returns (ListEntriesResponse); -BatchCreateEntry(BatchCreateEntryRequest) returns (BatchCreateEntryResponse); -GetEntry(GetEntryRequest) returns (spire.types.Entry); - -*/ - -type ListSelectorsRequest struct{} -type ListSelectorsResponse tornjakTypes.AgentInfoList - -// ListSelectors returns list of agents from the local DB with the following info -// spiffeid string -// plugin string -func (s *Server) ListSelectors(inp ListSelectorsRequest) (*ListSelectorsResponse, error) { - resp, err := s.Db.GetAgentSelectors() - if err != nil { - return nil, err - } - return (*ListSelectorsResponse)(&resp), nil -} - -type RegisterSelectorRequest tornjakTypes.AgentInfo - -// DefineSelectors registers an agent to the local DB with the following info -// spiffeid string -// plugin string -func (s *Server) DefineSelectors(inp RegisterSelectorRequest) error { - sinfo := tornjakTypes.AgentInfo(inp) - if len(sinfo.Spiffeid) == 0 { - return errors.New("agent's info missing mandatory field - Spiffeid") - } - return s.Db.CreateAgentEntry(sinfo) -} - -type ListAgentMetadataRequest tornjakTypes.AgentMetadataRequest -type ListAgentMetadataResponse tornjakTypes.AgentInfoList - -// ListAgentMetadata takes in list of agent spiffeids -// and returns list of those agents from the local DB with following info -// spiffeid string -// plugin string -// cluster string -// if no metadata found, no row is included -// if no spiffeids are specified, all agent metadata is returned -func (s *Server) ListAgentMetadata(inp ListAgentMetadataRequest) (*ListAgentMetadataResponse, error) { - inpReq := tornjakTypes.AgentMetadataRequest(inp) - resp, err := s.Db.GetAgentsMetadata(inpReq) - if err != nil { - return nil, err - } - return (*ListAgentMetadataResponse)(&resp), nil -} - -type ListClustersRequest struct{} -type ListClustersResponse tornjakTypes.ClusterInfoList - -// ListClusters returns list of clusters from the local DB with the following info -// name string -// details json -func (s *Server) ListClusters(inp ListClustersRequest) (*ListClustersResponse, error) { - retVal, err := s.Db.GetClusters() - if err != nil { - return nil, err - } - return (*ListClustersResponse)(&retVal), nil -} - -type RegisterClusterRequest tornjakTypes.ClusterInput - -// DefineCluster registers cluster to local DB -func (s *Server) DefineCluster(inp RegisterClusterRequest) error { - cinfo := tornjakTypes.ClusterInfo(inp.ClusterInstance) - if len(cinfo.Name) == 0 { - return errors.New("cluster definition missing mandatory field - Name") - } else if len(cinfo.PlatformType) == 0 { - return errors.New("cluster definition missing mandatory field - PlatformType") - } else if len(cinfo.EditedName) > 0 { - return errors.New("cluster definition attempts renaming on create cluster - EditedName") - } - return s.Db.CreateClusterEntry(cinfo) -} - -type EditClusterRequest tornjakTypes.ClusterInput - -// EditCluster registers cluster to local DB -func (s *Server) EditCluster(inp EditClusterRequest) error { - cinfo := tornjakTypes.ClusterInfo(inp.ClusterInstance) - if len(cinfo.Name) == 0 { - return errors.New("cluster definition missing mandatory field - Name") - } else if len(cinfo.PlatformType) == 0 { - return errors.New("cluster definition missing mandatory field - PlatformType") - } else if len(cinfo.EditedName) == 0 { - return errors.New("cluster definition missing mandatory field - EditedName") - } - return s.Db.EditClusterEntry(cinfo) -} - -type DeleteClusterRequest tornjakTypes.ClusterInput - -// DeleteCluster deletes cluster with name cinfo.Name and assignment to agents -func (s *Server) DeleteCluster(inp DeleteClusterRequest) error { - cinfo := tornjakTypes.ClusterInfo(inp.ClusterInstance) - if len(cinfo.Name) == 0 { - return errors.New("input missing mandatory field - Name") - } - return s.Db.DeleteClusterEntry(cinfo.Name) -} diff --git a/api/agent/tornjak_apis.go b/api/agent/tornjak_apis.go new file mode 100644 index 00000000..16392ec8 --- /dev/null +++ b/api/agent/tornjak_apis.go @@ -0,0 +1,125 @@ +package api + +import ( + "errors" + + tornjakTypes "github.com/spiffe/tornjak/pkg/agent/types" +) + +/* + +Agent + +ListAgents(ListAgentsRequest) returns (ListAgentsResponse); +BanAgent(BanAgentRequest) returns (google.protobuf.Empty); +DeleteAgent(DeleteAgentRequest) returns (google.protobuf.Empty); +CreateJoinToken(CreateJoinTokenRequest) returns (spire.types.JoinToken); + +Entries + +ListEntries(ListEntriesRequest) returns (ListEntriesResponse); +BatchCreateEntry(BatchCreateEntryRequest) returns (BatchCreateEntryResponse); +GetEntry(GetEntryRequest) returns (spire.types.Entry); + +*/ + +type ListSelectorsRequest struct{} +type ListSelectorsResponse tornjakTypes.AgentInfoList + +// ListSelectors returns list of agents from the local DB with the following info +// spiffeid string +// plugin string +func (s *Server) ListSelectors(inp ListSelectorsRequest) (*ListSelectorsResponse, error) { + resp, err := s.Db.GetAgentSelectors() + if err != nil { + return nil, err + } + return (*ListSelectorsResponse)(&resp), nil +} + +type RegisterSelectorRequest tornjakTypes.AgentInfo + +// DefineSelectors registers an agent to the local DB with the following info +// spiffeid string +// plugin string +func (s *Server) DefineSelectors(inp RegisterSelectorRequest) error { + sinfo := tornjakTypes.AgentInfo(inp) + if len(sinfo.Spiffeid) == 0 { + return errors.New("agent's info missing mandatory field - Spiffeid") + } + return s.Db.CreateAgentEntry(sinfo) +} + +type ListAgentMetadataRequest tornjakTypes.AgentMetadataRequest +type ListAgentMetadataResponse tornjakTypes.AgentInfoList + +// ListAgentMetadata takes in list of agent spiffeids +// and returns list of those agents from the local DB with following info +// spiffeid string +// plugin string +// cluster string +// if no metadata found, no row is included +// if no spiffeids are specified, all agent metadata is returned +func (s *Server) ListAgentMetadata(inp ListAgentMetadataRequest) (*ListAgentMetadataResponse, error) { + inpReq := tornjakTypes.AgentMetadataRequest(inp) + resp, err := s.Db.GetAgentsMetadata(inpReq) + if err != nil { + return nil, err + } + return (*ListAgentMetadataResponse)(&resp), nil +} + +type ListClustersRequest struct{} +type ListClustersResponse tornjakTypes.ClusterInfoList + +// ListClusters returns list of clusters from the local DB with the following info +// name string +// details json +func (s *Server) ListClusters(inp ListClustersRequest) (*ListClustersResponse, error) { + retVal, err := s.Db.GetClusters() + if err != nil { + return nil, err + } + return (*ListClustersResponse)(&retVal), nil +} + +type RegisterClusterRequest tornjakTypes.ClusterInput + +// DefineCluster registers cluster to local DB +func (s *Server) DefineCluster(inp RegisterClusterRequest) error { + cinfo := tornjakTypes.ClusterInfo(inp.ClusterInstance) + if len(cinfo.Name) == 0 { + return errors.New("cluster definition missing mandatory field - Name") + } else if len(cinfo.PlatformType) == 0 { + return errors.New("cluster definition missing mandatory field - PlatformType") + } else if len(cinfo.EditedName) > 0 { + return errors.New("cluster definition attempts renaming on create cluster - EditedName") + } + return s.Db.CreateClusterEntry(cinfo) +} + +type EditClusterRequest tornjakTypes.ClusterInput + +// EditCluster registers cluster to local DB +func (s *Server) EditCluster(inp EditClusterRequest) error { + cinfo := tornjakTypes.ClusterInfo(inp.ClusterInstance) + if len(cinfo.Name) == 0 { + return errors.New("cluster definition missing mandatory field - Name") + } else if len(cinfo.PlatformType) == 0 { + return errors.New("cluster definition missing mandatory field - PlatformType") + } else if len(cinfo.EditedName) == 0 { + return errors.New("cluster definition missing mandatory field - EditedName") + } + return s.Db.EditClusterEntry(cinfo) +} + +type DeleteClusterRequest tornjakTypes.ClusterInput + +// DeleteCluster deletes cluster with name cinfo.Name and assignment to agents +func (s *Server) DeleteCluster(inp DeleteClusterRequest) error { + cinfo := tornjakTypes.ClusterInfo(inp.ClusterInstance) + if len(cinfo.Name) == 0 { + return errors.New("input missing mandatory field - Name") + } + return s.Db.DeleteClusterEntry(cinfo.Name) +} From 681f68c5d02406d82aa11c88eeb9a01e390391fc Mon Sep 17 00:00:00 2001 From: Maia Iyer Date: Mon, 30 Sep 2024 12:52:46 -0400 Subject: [PATCH 2/4] refactored api/agent/server.go Signed-off-by: Maia Iyer --- api/agent/config.go | 267 +++++++ api/agent/handlers.go | 1018 +++++++++++++++++++++++++ api/agent/server.go | 1690 ++++++----------------------------------- 3 files changed, 1498 insertions(+), 1477 deletions(-) create mode 100644 api/agent/config.go create mode 100644 api/agent/handlers.go diff --git a/api/agent/config.go b/api/agent/config.go new file mode 100644 index 00000000..34086802 --- /dev/null +++ b/api/agent/config.go @@ -0,0 +1,267 @@ +package api + +import ( + "fmt" + "strings" + "time" + + backoff "github.com/cenkalti/backoff/v4" + "github.com/hashicorp/hcl" + "github.com/hashicorp/hcl/hcl/ast" + "github.com/hashicorp/hcl/hcl/token" + "github.com/pkg/errors" + + "github.com/spiffe/tornjak/pkg/agent/authentication/authenticator" + "github.com/spiffe/tornjak/pkg/agent/authorization" + agentdb "github.com/spiffe/tornjak/pkg/agent/db" +) + +func stringFromToken(keyToken token.Token) (string, error) { + switch keyToken.Type { + case token.STRING, token.IDENT: + default: + return "", fmt.Errorf("expected STRING or IDENT but got %s", keyToken.Type) + } + value := keyToken.Value() + stringValue, ok := value.(string) + if !ok { + // purely defensive + return "", fmt.Errorf("expected %T but got %T", stringValue, value) + } + return stringValue, nil +} + +// getPluginConfig returns first plugin configuration +func getPluginConfig(plugin *ast.ObjectItem) (string, ast.Node, error) { + // extract plugin name and value + pluginName, err := stringFromToken(plugin.Keys[1].Token) + if err != nil { + return "", nil, fmt.Errorf("invalid plugin type name %q: %w", plugin.Keys[1].Token.Text, err) + } + // extract data + var hclPluginConfig hclPluginConfig + if err := hcl.DecodeObject(&hclPluginConfig, plugin.Val); err != nil { + return "", nil, fmt.Errorf("failed to decode plugin config for %q: %w", pluginName, err) + } + return pluginName, hclPluginConfig.PluginData, nil +} + +// NewAgentsDB returns a new agents DB, given a DB connection string +func NewAgentsDB(dbPlugin *ast.ObjectItem) (agentdb.AgentDB, error) { + key, data, err := getPluginConfig(dbPlugin) + if err != nil { // db is required config + return nil, errors.New("Required DataStore plugin not configured") + } + + switch key { + case "sql": + // check if data is defined + if data == nil { + return nil, errors.New("SQL DataStore plugin ('config > plugins > DataStore sql > plugin_data') not populated") + } + fmt.Printf("SQL DATASTORE DATA: %+v\n", data) + + // TODO can probably add this to config + expBackoff := backoff.NewExponentialBackOff() + expBackoff.MaxElapsedTime = time.Second + + // decode config to struct + var config pluginDataStoreSQL + if err := hcl.DecodeObject(&config, data); err != nil { + return nil, errors.Errorf("Couldn't parse DB config: %v", err) + } + + // create db + drivername := config.Drivername + dbfile := config.Filename + + db, err := agentdb.NewLocalSqliteDB(drivername, dbfile, expBackoff) + if err != nil { + return nil, errors.Errorf("Could not start DB driver %s, filename: %s: %v", drivername, dbfile, err) + } + return db, nil + default: + return nil, errors.Errorf("Couldn't create datastore") + } +} + +// NewAuthenticator returns a new Authenticator +func NewAuthenticator(authenticatorPlugin *ast.ObjectItem) (authenticator.Authenticator, error) { + key, data, _ := getPluginConfig(authenticatorPlugin) + + switch key { + case "Keycloak": + // check if data is defined + if data == nil { + return nil, errors.New("Keycloak Authenticator plugin ('config > plugins > Authenticator Keycloak > plugin_data') not populated") + } + fmt.Printf("Authenticator Keycloak Plugin Data: %+v\n", data) + // decode config to struct + var config pluginAuthenticatorKeycloak + if err := hcl.DecodeObject(&config, data); err != nil { + return nil, errors.Errorf("Couldn't parse Authenticator config: %v", err) + } + + // Log warning if audience is nil that aud claim is not checked + if config.Audience == "" { + fmt.Println("WARNING: Auth plugin has no expected audience configured - `aud` claim will not be checked (please populate 'config > plugins > UserManagement KeycloakAuth > plugin_data > audience')") + } + + // create authenticator TODO make json an option? + authenticator, err := authenticator.NewKeycloakAuthenticator(true, config.IssuerURL, config.Audience) + if err != nil { + return nil, errors.Errorf("Couldn't configure Authenticator: %v", err) + } + return authenticator, nil + default: + return nil, errors.Errorf("Invalid option for Authenticator named %s", key) + } +} + +// NewAuthorizer returns a new Authorizer +func NewAuthorizer(authorizerPlugin *ast.ObjectItem) (authorization.Authorizer, error) { + key, data, _ := getPluginConfig(authorizerPlugin) + + switch key { + case "RBAC": + // check if data is defined + if data == nil { + return nil, errors.New("RBAC Authorizer plugin ('config > plugins > Authorizer RBAC > plugin_data') not populated") + } + fmt.Printf("Authorizer RBAC Plugin Data: %+v\n", data) + + // decode config to struct + var config pluginAuthorizerRBAC + if err := hcl.DecodeObject(&config, data); err != nil { + return nil, errors.Errorf("Couldn't parse Authorizer config: %v", err) + } + + // 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 + if role.Name == "" { + fmt.Println("WARNING: using the empty string for an API enables access to all authenticated users") + } + } + 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, apiV1Mapping) + if err != nil { + return nil, errors.Errorf("Couldn't configure Authorizer: %v", err) + } + return authorizer, nil + default: + return nil, errors.Errorf("Invalid option for Authorizer named %s", key) + } +} + +func (s *Server) VerifyConfiguration() error { + if s.TornjakConfig == nil { + return errors.New("config not given") + } + + /* Verify server */ + if s.TornjakConfig.Server == nil { // must be defined + return errors.New("'config > server' field not defined") + } + if s.TornjakConfig.Server.SPIRESocket == "" { + return errors.New("'config > server > spire_socket_path' field not defined") + } + + /* Verify Plugins */ + if s.TornjakConfig.Plugins == nil { + return errors.New("'config > plugins' field not defined") + } + return nil +} + +func (s *Server) ConfigureDefaults() error { + // no authorization is a default + s.Authenticator = authenticator.NewNullAuthenticator() + s.Authorizer = authorization.NewNullAuthorizer() + return nil +} + +func (s *Server) Configure() error { + // Verify Config + err := s.VerifyConfiguration() + if err != nil { + return errors.Errorf("Tornjak Config error: %v", err) + } + + /* Configure Server */ + serverConfig := s.TornjakConfig.Server + s.SpireServerAddr = serverConfig.SPIRESocket // for convenience + + /* Configure Plugins */ + // configure defaults for optional plugins, reconfigured if given + // TODO maybe we should not have this step at all + // This is a temporary work around for optional plugin configs + err = s.ConfigureDefaults() + if err != nil { + return errors.Errorf("Tornjak Config error: %v", err) + } + + pluginConfigs := *s.TornjakConfig.Plugins + pluginList, ok := pluginConfigs.(*ast.ObjectList) + if !ok { + return fmt.Errorf("expected plugins node type %T but got %T", pluginList, pluginConfigs) + } + + // iterate over plugin list + + for _, pluginObject := range pluginList.Items { + if len(pluginObject.Keys) != 2 { + return fmt.Errorf("plugin item expected to have two keys (type then name)") + } + + pluginType, err := stringFromToken(pluginObject.Keys[0].Token) + if err != nil { + return fmt.Errorf("invalid plugin type key %q: %w", pluginObject.Keys[0].Token.Text, err) + } + + // create plugin component based on type + switch pluginType { + // configure datastore + case "DataStore": + s.Db, err = NewAgentsDB(pluginObject) + if err != nil { + return errors.Errorf("Cannot configure datastore plugin: %v", err) + } + // configure Authenticator + case "Authenticator": + s.Authenticator, err = NewAuthenticator(pluginObject) + if err != nil { + return errors.Errorf("Cannot configure Authenticator plugin: %v", err) + } + // configure Authorizer + case "Authorizer": + s.Authorizer, err = NewAuthorizer(pluginObject) + if err != nil { + return errors.Errorf("Cannot configure Authorizer plugin: %v", err) + } + } + // TODO Handle when multiple plugins configured + } + + return nil +} diff --git a/api/agent/handlers.go b/api/agent/handlers.go new file mode 100644 index 00000000..d086abd7 --- /dev/null +++ b/api/agent/handlers.go @@ -0,0 +1,1018 @@ +package api + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + + trustdomain "github.com/spiffe/spire-api-sdk/proto/spire/api/server/trustdomain/v1" + "google.golang.org/protobuf/encoding/protojson" +) + +func (s *Server) healthcheck(w http.ResponseWriter, r *http.Request) { + var input HealthcheckRequest + 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 = HealthcheckRequest{} + } 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.SPIREHealthcheck(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) debugServer(w http.ResponseWriter, r *http.Request) { + input := DebugServerRequest{} // HARDCODED INPUT because there are no fields to DebugServerRequest + + ret, err := s.DebugServer(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) agentList(w http.ResponseWriter, r *http.Request) { + var input ListAgentsRequest + 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 = ListAgentsRequest{} + } 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.ListAgents(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) agentBan(w http.ResponseWriter, r *http.Request) { + var input BanAgentRequest + 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 { + emsg := "Error: no data provided" + retError(w, emsg, http.StatusBadRequest) + return + } 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 + } + } + + err = s.BanAgent(input) //nolint:govet //Ignoring mutex (not being used) - sync.Mutex by value is unused for linter govet + if err != nil { + emsg := fmt.Sprintf("Error listing agents: %v", err.Error()) + retError(w, emsg, http.StatusInternalServerError) + return + } + + cors(w, r) + _, err = w.Write([]byte("SUCCESS")) + + if err != nil { + emsg := fmt.Sprintf("Error: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + return + } + +} + +func (s *Server) agentDelete(w http.ResponseWriter, r *http.Request) { + // TODO update backend to also delete agent metadata + + var input DeleteAgentRequest + 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 { + emsg := "Error: no data provided" + retError(w, emsg, http.StatusBadRequest) + return + } 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 + } + } + + err = s.DeleteAgent(input) //nolint:govet //Ignoring mutex (not being used) - sync.Mutex by value is unused for linter govet + if err != nil { + emsg := fmt.Sprintf("Error listing agents: %v", err.Error()) + retError(w, emsg, http.StatusInternalServerError) + return + } + + cors(w, r) + _, err = w.Write([]byte("SUCCESS")) + + if err != nil { + emsg := fmt.Sprintf("Error: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + return + } + +} + +func (s *Server) agentCreateJoinToken(w http.ResponseWriter, r *http.Request) { + var input CreateJoinTokenRequest + 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 = CreateJoinTokenRequest{} + } 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.CreateJoinToken(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) entryList(w http.ResponseWriter, r *http.Request) { + var input ListEntriesRequest + 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 = ListEntriesRequest{} + } 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.ListEntries(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) entryCreate(w http.ResponseWriter, r *http.Request) { + var input BatchCreateEntryRequest + 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 = BatchCreateEntryRequest{} + } 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.BatchCreateEntry(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) entryDelete(w http.ResponseWriter, r *http.Request) { + var input BatchDeleteEntryRequest + 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 = BatchDeleteEntryRequest{} + } 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.BatchDeleteEntry(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 + } +} + +// 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 + } +} + +// Tornjak Handlers +func (s *Server) home(w http.ResponseWriter, r *http.Request) { + var ret = "Welcome to the Tornjak Backend!" + + cors(w, r) + je := json.NewEncoder(w) + + var err = je.Encode(ret) + if err != nil { + emsg := fmt.Sprintf("Error: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + } +} + +func (s *Server) health(w http.ResponseWriter, r *http.Request) { + var ret = "Endpoint is healthy." + + cors(w, r) + je := json.NewEncoder(w) + + var err = je.Encode(ret) + if err != nil { + emsg := fmt.Sprintf("Error: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + } +} + +func (s *Server) tornjakSelectorsList(w http.ResponseWriter, r *http.Request) { + 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() + var input ListSelectorsRequest + if n == 0 { + input = ListSelectorsRequest{} + } 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.ListSelectors(input) + if err != nil { + emsg := fmt.Sprintf("Error: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + 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) tornjakPluginDefine(w http.ResponseWriter, r *http.Request) { + 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() + var input RegisterSelectorRequest + if n == 0 { + input = RegisterSelectorRequest{} + } 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 + } + } + err = s.DefineSelectors(input) + if err != nil { + emsg := fmt.Sprintf("Error: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + return + } + cors(w, r) + _, err = w.Write([]byte("SUCCESS")) + if err != nil { + emsg := fmt.Sprintf("Error: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + return + } +} + +func (s *Server) tornjakAgentsList(w http.ResponseWriter, r *http.Request) { + 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() + var input ListAgentMetadataRequest + if n == 0 { + input = ListAgentMetadataRequest{} + } 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.ListAgentMetadata(input) + if err != nil { + emsg := fmt.Sprintf("Error: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + 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 + } +} + +/********* CLUSTER *********/ + +func (s *Server) clusterList(w http.ResponseWriter, r *http.Request) { + var input ListClustersRequest + 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 = ListClustersRequest{} + } 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.ListClusters(input) + if err != nil { + emsg := fmt.Sprintf("Error: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + 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) clusterCreate(w http.ResponseWriter, r *http.Request) { + 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() + var input RegisterClusterRequest + if n == 0 { + input = RegisterClusterRequest{} + } 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 + } + } + err = s.DefineCluster(input) + if err != nil { + emsg := fmt.Sprintf("Error: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + return + } + cors(w, r) + _, err = w.Write([]byte("SUCCESS")) + if err != nil { + emsg := fmt.Sprintf("Error: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + return + } +} + +func (s *Server) clusterEdit(w http.ResponseWriter, r *http.Request) { + 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() + var input EditClusterRequest + if n == 0 { + input = EditClusterRequest{} + } 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 + } + } + err = s.EditCluster(input) + if err != nil { + emsg := fmt.Sprintf("Error: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + return + } + cors(w, r) + _, err = w.Write([]byte("SUCCESS")) + if err != nil { + emsg := fmt.Sprintf("Error: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + return + } +} + +func (s *Server) clusterDelete(w http.ResponseWriter, r *http.Request) { + 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() + var input DeleteClusterRequest + if n == 0 { + input = DeleteClusterRequest{} + } 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 + } + } + err = s.DeleteCluster(input) + if err != nil { + emsg := fmt.Sprintf("Error: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + return + } + cors(w, r) + _, err = w.Write([]byte("SUCCESS")) + if err != nil { + emsg := fmt.Sprintf("Error: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + return + } + +} + +/********* END CLUSTER *********/ diff --git a/api/agent/server.go b/api/agent/server.go index cbb9a835..3ca81726 100644 --- a/api/agent/server.go +++ b/api/agent/server.go @@ -11,17 +11,9 @@ import ( "os" "path/filepath" "strings" - "time" - backoff "github.com/cenkalti/backoff/v4" "github.com/gorilla/mux" - "github.com/hashicorp/hcl" "github.com/hashicorp/hcl/hcl/ast" - "github.com/hashicorp/hcl/hcl/token" - "github.com/pkg/errors" - - "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" @@ -54,70 +46,49 @@ type hclPluginConfig struct { Enabled *bool `hcl:"enabled"` } -func (s *Server) healthcheck(w http.ResponseWriter, r *http.Request) { - var input HealthcheckRequest - buf := new(strings.Builder) +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, 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) +} - 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() +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, 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) +} - if n == 0 { - input = HealthcheckRequest{} - } else { - err := json.Unmarshal([]byte(data), &input) - if err != nil { - emsg := fmt.Sprintf("Error parsing data: %v", err.Error()) - retError(w, emsg, http.StatusBadRequest) +// Handle preflight checks +func (s *Server) verificationMiddleware(next http.Handler) http.Handler { + f := func(w http.ResponseWriter, r *http.Request) { + if r.Method == "OPTIONS" { + cors(w, r) return } - } - - ret, err := s.SPIREHealthcheck(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) debugServer(w http.ResponseWriter, r *http.Request) { - input := DebugServerRequest{} // HARDCODED INPUT because there are no fields to DebugServerRequest - ret, err := s.DebugServer(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 - } + userInfo := s.Authenticator.AuthenticateRequest(r) - cors(w, r) - je := json.NewEncoder(w) + err := s.Authorizer.AuthorizeRequest(r, userInfo) + if err != nil { + emsg := fmt.Sprintf("Error authorizing request: %v", err.Error()) + // error should be written already + retError(w, emsg, http.StatusUnauthorized) + return + } - err = je.Encode(ret) - if err != nil { - emsg := fmt.Sprintf("Error: %v", err.Error()) - retError(w, emsg, http.StatusBadRequest) - return + next.ServeHTTP(w, r) } + return http.HandlerFunc(f) } -func (s *Server) agentList(w http.ResponseWriter, r *http.Request) { - var input ListAgentsRequest +func (s *Server) tornjakGetServerInfo(w http.ResponseWriter, r *http.Request) { + var input GetTornjakServerInfoRequest buf := new(strings.Builder) n, err := io.Copy(buf, r.Body) @@ -129,7 +100,7 @@ func (s *Server) agentList(w http.ResponseWriter, r *http.Request) { data := buf.String() if n == 0 { - input = ListAgentsRequest{} + input = GetTornjakServerInfoRequest{} } else { err := json.Unmarshal([]byte(data), &input) if err != nil { @@ -139,1482 +110,247 @@ func (s *Server) agentList(w http.ResponseWriter, r *http.Request) { } } - ret, err := s.ListAgents(input) //nolint:govet //Ignoring mutex (not being used) - sync.Mutex by value is unused for linter govet + ret, err := s.GetTornjakServerInfo(input) if err != nil { + // The error occurs only when serverinfo is empty + // This indicates --spire-config not passed + // return 204 for no content emsg := fmt.Sprintf("Error: %v", err.Error()) - retError(w, emsg, http.StatusInternalServerError) + retError(w, emsg, http.StatusNoContent) 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) agentBan(w http.ResponseWriter, r *http.Request) { - var input BanAgentRequest - buf := new(strings.Builder) +// spaHandler implements the http.Handler interface, so we can use it +// to respond to HTTP requests. The path to the static directory and +// path to the index file within that static directory are used to +// serve the SPA in the given static directory. +type spaHandler struct { + staticPath string + indexPath string +} - n, err := io.Copy(buf, r.Body) +// ServeHTTP inspects the URL path to locate a file within the static dir +// on the SPA handler. If a file is found, it will be served. If not, the +// file located at the index path on the SPA handler will be served. This +// is suitable behavior for serving an SPA (single page application). +func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // get the absolute path to prevent directory traversal + path, err := filepath.Abs(r.URL.Path) if err != nil { - emsg := fmt.Sprintf("Error parsing data: %v", err.Error()) - retError(w, emsg, http.StatusBadRequest) + // if we failed to get the absolute path respond with a 400 bad request + // and stop + http.Error(w, err.Error(), http.StatusBadRequest) return } - data := buf.String() - if n == 0 { - emsg := "Error: no data provided" - retError(w, emsg, http.StatusBadRequest) - return - } 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 - } - } + // prepend the path with the path to the static directory + path = filepath.Join(h.staticPath, path) - err = s.BanAgent(input) //nolint:govet //Ignoring mutex (not being used) - sync.Mutex by value is unused for linter govet - if err != nil { - emsg := fmt.Sprintf("Error listing agents: %v", err.Error()) - retError(w, emsg, http.StatusInternalServerError) + // check whether a file exists at the given path + _, err = os.Stat(path) + if os.IsNotExist(err) { + // file does not exist, serve index.html + http.ServeFile(w, r, filepath.Join(h.staticPath, h.indexPath)) + return + } else if err != nil { + // if we got an error (that wasn't that the file doesn't exist) stating the + // file, return a 500 internal server error and stop + http.Error(w, err.Error(), http.StatusInternalServerError) return } + // otherwise, use http.FileServer to serve the static dir + http.FileServer(http.Dir(h.staticPath)).ServeHTTP(w, r) +} - cors(w, r) - _, err = w.Write([]byte("SUCCESS")) +func (s *Server) GetRouter() http.Handler { + rtr := mux.NewRouter() - if err != nil { - emsg := fmt.Sprintf("Error: %v", err.Error()) - retError(w, emsg, http.StatusBadRequest) - return - } + apiRtr := rtr.PathPrefix("/").Subrouter() + healthRtr := rtr.PathPrefix("/healthz").Subrouter() -} + // Healthcheck (never goes through authn/authz layers) + healthRtr.HandleFunc("", s.health) -func (s *Server) agentDelete(w http.ResponseWriter, r *http.Request) { - // TODO update backend to also delete agent metadata + // Home + apiRtr.HandleFunc("/", s.home) - var input DeleteAgentRequest - buf := new(strings.Builder) + // SPIRE server healthcheck + apiRtr.HandleFunc("/api/debugserver", s.debugServer) + apiRtr.HandleFunc("/api/healthcheck", s.healthcheck) - 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() + // Agents + apiRtr.HandleFunc("/api/agent/list", s.agentList) + apiRtr.HandleFunc("/api/agent/ban", s.agentBan) + apiRtr.HandleFunc("/api/agent/delete", s.agentDelete) + apiRtr.HandleFunc("/api/agent/createjointoken", s.agentCreateJoinToken) - if n == 0 { - emsg := "Error: no data provided" - retError(w, emsg, http.StatusBadRequest) - return - } 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 - } - } + // Entries + apiRtr.HandleFunc("/api/entry/list", s.entryList) + apiRtr.HandleFunc("/api/entry/create", s.entryCreate) + apiRtr.HandleFunc("/api/entry/delete", s.entryDelete) - err = s.DeleteAgent(input) //nolint:govet //Ignoring mutex (not being used) - sync.Mutex by value is unused for linter govet - if err != nil { - emsg := fmt.Sprintf("Error listing agents: %v", err.Error()) - retError(w, emsg, http.StatusInternalServerError) - return - } + // Tornjak specific + apiRtr.HandleFunc("/api/tornjak/serverinfo", s.tornjakGetServerInfo) + // Agents Selectors + apiRtr.HandleFunc("/api/tornjak/selectors/register", s.tornjakPluginDefine) + apiRtr.HandleFunc("/api/tornjak/selectors/list", s.tornjakSelectorsList) + apiRtr.HandleFunc("/api/tornjak/agents/list", s.tornjakAgentsList) + // Clusters + apiRtr.HandleFunc("/api/tornjak/clusters/list", s.clusterList) + apiRtr.HandleFunc("/api/tornjak/clusters/create", s.clusterCreate) + apiRtr.HandleFunc("/api/tornjak/clusters/edit", s.clusterEdit) + apiRtr.HandleFunc("/api/tornjak/clusters/delete", s.clusterDelete) - cors(w, r) - _, err = w.Write([]byte("SUCCESS")) + // 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) - if err != nil { - emsg := fmt.Sprintf("Error: %v", err.Error()) - retError(w, emsg, http.StatusBadRequest) - return - } + // 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) -func (s *Server) agentCreateJoinToken(w http.ResponseWriter, r *http.Request) { - var input CreateJoinTokenRequest - buf := new(strings.Builder) + // UI + spa := spaHandler{staticPath: "ui-agent", indexPath: "index.html"} + rtr.PathPrefix("/").Handler(spa) - 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 rtr +} + +func (s *Server) redirectHTTP(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet && r.Method != "HEAD" { + http.Error(w, "Use HTTPS", http.StatusBadRequest) return } - data := buf.String() + target := "https://" + s.stripPort(r.Host) + r.URL.RequestURI() + http.Redirect(w, r, target, http.StatusFound) +} - if n == 0 { - input = CreateJoinTokenRequest{} - } 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.CreateJoinToken(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) +func (s *Server) stripPort(hostport string) string { + host, _, err := net.SplitHostPort(hostport) if err != nil { - emsg := fmt.Sprintf("Error: %v", err.Error()) - retError(w, emsg, http.StatusBadRequest) - return + return hostport } - + addr := fmt.Sprintf("%d", s.TornjakConfig.Server.HTTPSConfig.ListenPort) + return net.JoinHostPort(host, addr) } -func (s *Server) entryList(w http.ResponseWriter, r *http.Request) { - var input ListEntriesRequest - buf := new(strings.Builder) - - n, err := io.Copy(buf, r.Body) +// HandleRequests connects api links with respective functions +// Functions currently handle the api calls all as post-requests +func (s *Server) HandleRequests() { + err := s.Configure() 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 = ListEntriesRequest{} - } 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 - } + log.Fatal("Cannot Configure: ", err) } - ret, err := s.ListEntries(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 - } + // TODO: replace with workerGroup for thread safety + errChannel := make(chan error, 2) - 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) + serverConfig := s.TornjakConfig.Server + if serverConfig.HTTPConfig == nil { + err = fmt.Errorf("HTTP Config error: no port configured") + errChannel <- err return } -} - -func (s *Server) entryCreate(w http.ResponseWriter, r *http.Request) { - var input BatchCreateEntryRequest - 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() + // default router does not redirect + httpHandler := s.GetRouter() + numPorts := 1 - if n == 0 { - input = BatchCreateEntryRequest{} + if serverConfig.HTTPSConfig == nil { // warn when HTTPS not configured + log.Print("WARNING: Please consider configuring HTTPS to ensure traffic is running on encrypted endpoint!") } 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.BatchCreateEntry(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) entryDelete(w http.ResponseWriter, r *http.Request) { - var input BatchDeleteEntryRequest - buf := new(strings.Builder) + numPorts += 1 - 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() + httpHandler = http.HandlerFunc(s.redirectHTTP) + canStartHTTPS := true + httpsConfig := serverConfig.HTTPSConfig + var tlsConfig *tls.Config - if n == 0 { - input = BatchDeleteEntryRequest{} - } 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 + 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) + errChannel <- err + httpHandler = s.GetRouter() + canStartHTTPS = false + } else { + tlsConfig, err = httpsConfig.Parse() + if err != nil { + err = fmt.Errorf("failed parsing HTTPS config: %w. Starting insecure HTTP connection at %d...", err, serverConfig.HTTPConfig.ListenPort) + errChannel <- err + httpHandler = s.GetRouter() + canStartHTTPS = false + } } - } - - ret, err := s.BatchDeleteEntry(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 - } -} -// 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 canStartHTTPS { + go func() { + addr := fmt.Sprintf(":%d", serverConfig.HTTPSConfig.ListenPort) + // Create a Server instance to listen on port 8443 with the TLS config + server := &http.Server{ + Handler: s.GetRouter(), + Addr: addr, + TLSConfig: tlsConfig, + } - 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 + fmt.Printf("Starting https on %s...\n", addr) + err = server.ListenAndServeTLS(httpsConfig.Cert, httpsConfig.Key) + if err != nil { + err = fmt.Errorf("server error serving on https: %w", err) + errChannel <- err + } + }() } } - 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) + go func() { + addr := fmt.Sprintf(":%d", serverConfig.HTTPConfig.ListenPort) + fmt.Printf("Starting to listen on %s...\n", addr) + err := http.ListenAndServe(addr, httpHandler) if err != nil { - emsg := fmt.Sprintf("Error parsing data: %v", err.Error()) - retError(w, emsg, http.StatusBadRequest) - return + errChannel <- err } - } - - 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 + // as errors come in, read them, and block + for i := 0; i < numPorts; i++ { + err := <-errChannel + log.Printf("%v", err) } } - -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, 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) -} - -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, 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) -} - -// Handle preflight checks -func (s *Server) verificationMiddleware(next http.Handler) http.Handler { - f := func(w http.ResponseWriter, r *http.Request) { - if r.Method == "OPTIONS" { - cors(w, r) - return - } - - userInfo := s.Authenticator.AuthenticateRequest(r) - - err := s.Authorizer.AuthorizeRequest(r, userInfo) - if err != nil { - emsg := fmt.Sprintf("Error authorizing request: %v", err.Error()) - // error should be written already - retError(w, emsg, http.StatusUnauthorized) - return - } - - next.ServeHTTP(w, r) - } - return http.HandlerFunc(f) -} - -func (s *Server) tornjakGetServerInfo(w http.ResponseWriter, r *http.Request) { - var input GetTornjakServerInfoRequest - 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 = GetTornjakServerInfoRequest{} - } 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.GetTornjakServerInfo(input) - if err != nil { - // The error occurs only when serverinfo is empty - // This indicates --spire-config not passed - // return 204 for no content - emsg := fmt.Sprintf("Error: %v", err.Error()) - retError(w, emsg, http.StatusNoContent) - 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 - } -} - -// spaHandler implements the http.Handler interface, so we can use it -// to respond to HTTP requests. The path to the static directory and -// path to the index file within that static directory are used to -// serve the SPA in the given static directory. -type spaHandler struct { - staticPath string - indexPath string -} - -// ServeHTTP inspects the URL path to locate a file within the static dir -// on the SPA handler. If a file is found, it will be served. If not, the -// file located at the index path on the SPA handler will be served. This -// is suitable behavior for serving an SPA (single page application). -func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - // get the absolute path to prevent directory traversal - path, err := filepath.Abs(r.URL.Path) - if err != nil { - // if we failed to get the absolute path respond with a 400 bad request - // and stop - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - // prepend the path with the path to the static directory - path = filepath.Join(h.staticPath, path) - - // check whether a file exists at the given path - _, err = os.Stat(path) - if os.IsNotExist(err) { - // file does not exist, serve index.html - http.ServeFile(w, r, filepath.Join(h.staticPath, h.indexPath)) - return - } else if err != nil { - // if we got an error (that wasn't that the file doesn't exist) stating the - // file, return a 500 internal server error and stop - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - // otherwise, use http.FileServer to serve the static dir - http.FileServer(http.Dir(h.staticPath)).ServeHTTP(w, r) -} - -func (s *Server) home(w http.ResponseWriter, r *http.Request) { - var ret = "Welcome to the Tornjak Backend!" - - cors(w, r) - je := json.NewEncoder(w) - - var err = je.Encode(ret) - if err != nil { - emsg := fmt.Sprintf("Error: %v", err.Error()) - retError(w, emsg, http.StatusBadRequest) - } -} - -func (s *Server) health(w http.ResponseWriter, r *http.Request) { - var ret = "Endpoint is healthy." - - cors(w, r) - je := json.NewEncoder(w) - - var err = je.Encode(ret) - if err != nil { - emsg := fmt.Sprintf("Error: %v", err.Error()) - retError(w, emsg, http.StatusBadRequest) - } -} - -func (s *Server) GetRouter() http.Handler { - rtr := mux.NewRouter() - - apiRtr := rtr.PathPrefix("/").Subrouter() - healthRtr := rtr.PathPrefix("/healthz").Subrouter() - - // Healthcheck (never goes through authn/authz layers) - healthRtr.HandleFunc("", s.health) - - // Home - apiRtr.HandleFunc("/", s.home) - - // SPIRE server healthcheck - apiRtr.HandleFunc("/api/debugserver", s.debugServer) - apiRtr.HandleFunc("/api/healthcheck", s.healthcheck) - - // Agents - apiRtr.HandleFunc("/api/agent/list", s.agentList) - apiRtr.HandleFunc("/api/agent/ban", s.agentBan) - apiRtr.HandleFunc("/api/agent/delete", s.agentDelete) - apiRtr.HandleFunc("/api/agent/createjointoken", s.agentCreateJoinToken) - - // Entries - apiRtr.HandleFunc("/api/entry/list", s.entryList) - apiRtr.HandleFunc("/api/entry/create", s.entryCreate) - apiRtr.HandleFunc("/api/entry/delete", s.entryDelete) - - // Tornjak specific - apiRtr.HandleFunc("/api/tornjak/serverinfo", s.tornjakGetServerInfo) - // Agents Selectors - apiRtr.HandleFunc("/api/tornjak/selectors/register", s.tornjakPluginDefine) - apiRtr.HandleFunc("/api/tornjak/selectors/list", s.tornjakSelectorsList) - apiRtr.HandleFunc("/api/tornjak/agents/list", s.tornjakAgentsList) - // Clusters - apiRtr.HandleFunc("/api/tornjak/clusters/list", s.clusterList) - apiRtr.HandleFunc("/api/tornjak/clusters/create", s.clusterCreate) - 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) - - // UI - spa := spaHandler{staticPath: "ui-agent", indexPath: "index.html"} - rtr.PathPrefix("/").Handler(spa) - - return rtr -} - -func (s *Server) redirectHTTP(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet && r.Method != "HEAD" { - http.Error(w, "Use HTTPS", http.StatusBadRequest) - return - } - target := "https://" + s.stripPort(r.Host) + r.URL.RequestURI() - http.Redirect(w, r, target, http.StatusFound) -} - -func (s *Server) stripPort(hostport string) string { - host, _, err := net.SplitHostPort(hostport) - if err != nil { - return hostport - } - addr := fmt.Sprintf("%d", s.TornjakConfig.Server.HTTPSConfig.ListenPort) - return net.JoinHostPort(host, addr) -} - -// HandleRequests connects api links with respective functions -// Functions currently handle the api calls all as post-requests -func (s *Server) HandleRequests() { - err := s.Configure() - if err != nil { - log.Fatal("Cannot Configure: ", err) - } - - // 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") - errChannel <- err - return - } - - // default router does not redirect - httpHandler := s.GetRouter() - numPorts := 1 - - if serverConfig.HTTPSConfig == nil { // warn when HTTPS not configured - log.Print("WARNING: Please consider configuring HTTPS to ensure traffic is running on encrypted endpoint!") - } else { - numPorts += 1 - - httpHandler = http.HandlerFunc(s.redirectHTTP) - canStartHTTPS := true - 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) - errChannel <- err - httpHandler = s.GetRouter() - canStartHTTPS = false - } else { - tlsConfig, err = httpsConfig.Parse() - if err != nil { - err = fmt.Errorf("failed parsing HTTPS config: %w. Starting insecure HTTP connection at %d...", err, serverConfig.HTTPConfig.ListenPort) - errChannel <- err - httpHandler = s.GetRouter() - canStartHTTPS = false - } - } - - if canStartHTTPS { - go func() { - addr := fmt.Sprintf(":%d", serverConfig.HTTPSConfig.ListenPort) - // Create a Server instance to listen on port 8443 with the TLS config - server := &http.Server{ - Handler: s.GetRouter(), - Addr: addr, - TLSConfig: tlsConfig, - } - - fmt.Printf("Starting https on %s...\n", addr) - err = server.ListenAndServeTLS(httpsConfig.Cert, httpsConfig.Key) - if err != nil { - err = fmt.Errorf("server error serving on https: %w", err) - errChannel <- err - } - }() - } - } - - go func() { - addr := fmt.Sprintf(":%d", serverConfig.HTTPConfig.ListenPort) - fmt.Printf("Starting to listen on %s...\n", addr) - err := http.ListenAndServe(addr, httpHandler) - if err != nil { - errChannel <- err - } - }() - - // as errors come in, read them, and block - for i := 0; i < numPorts; i++ { - err := <-errChannel - log.Printf("%v", err) - } -} - -func stringFromToken(keyToken token.Token) (string, error) { - switch keyToken.Type { - case token.STRING, token.IDENT: - default: - return "", fmt.Errorf("expected STRING or IDENT but got %s", keyToken.Type) - } - value := keyToken.Value() - stringValue, ok := value.(string) - if !ok { - // purely defensive - return "", fmt.Errorf("expected %T but got %T", stringValue, value) - } - return stringValue, nil -} - -// getPluginConfig returns first plugin configuration -func getPluginConfig(plugin *ast.ObjectItem) (string, ast.Node, error) { - // extract plugin name and value - pluginName, err := stringFromToken(plugin.Keys[1].Token) - if err != nil { - return "", nil, fmt.Errorf("invalid plugin type name %q: %w", plugin.Keys[1].Token.Text, err) - } - // extract data - var hclPluginConfig hclPluginConfig - if err := hcl.DecodeObject(&hclPluginConfig, plugin.Val); err != nil { - return "", nil, fmt.Errorf("failed to decode plugin config for %q: %w", pluginName, err) - } - return pluginName, hclPluginConfig.PluginData, nil -} - -// NewAgentsDB returns a new agents DB, given a DB connection string -func NewAgentsDB(dbPlugin *ast.ObjectItem) (agentdb.AgentDB, error) { - key, data, err := getPluginConfig(dbPlugin) - if err != nil { // db is required config - return nil, errors.New("Required DataStore plugin not configured") - } - - switch key { - case "sql": - // check if data is defined - if data == nil { - return nil, errors.New("SQL DataStore plugin ('config > plugins > DataStore sql > plugin_data') not populated") - } - fmt.Printf("SQL DATASTORE DATA: %+v\n", data) - - // TODO can probably add this to config - expBackoff := backoff.NewExponentialBackOff() - expBackoff.MaxElapsedTime = time.Second - - // decode config to struct - var config pluginDataStoreSQL - if err := hcl.DecodeObject(&config, data); err != nil { - return nil, errors.Errorf("Couldn't parse DB config: %v", err) - } - - // create db - drivername := config.Drivername - dbfile := config.Filename - - db, err := agentdb.NewLocalSqliteDB(drivername, dbfile, expBackoff) - if err != nil { - return nil, errors.Errorf("Could not start DB driver %s, filename: %s: %v", drivername, dbfile, err) - } - return db, nil - default: - return nil, errors.Errorf("Couldn't create datastore") - } -} - -// NewAuthenticator returns a new Authenticator -func NewAuthenticator(authenticatorPlugin *ast.ObjectItem) (authenticator.Authenticator, error) { - key, data, _ := getPluginConfig(authenticatorPlugin) - - switch key { - case "Keycloak": - // check if data is defined - if data == nil { - return nil, errors.New("Keycloak Authenticator plugin ('config > plugins > Authenticator Keycloak > plugin_data') not populated") - } - fmt.Printf("Authenticator Keycloak Plugin Data: %+v\n", data) - // decode config to struct - var config pluginAuthenticatorKeycloak - if err := hcl.DecodeObject(&config, data); err != nil { - return nil, errors.Errorf("Couldn't parse Authenticator config: %v", err) - } - - // Log warning if audience is nil that aud claim is not checked - if config.Audience == "" { - fmt.Println("WARNING: Auth plugin has no expected audience configured - `aud` claim will not be checked (please populate 'config > plugins > UserManagement KeycloakAuth > plugin_data > audience')") - } - - // create authenticator TODO make json an option? - authenticator, err := authenticator.NewKeycloakAuthenticator(true, config.IssuerURL, config.Audience) - if err != nil { - return nil, errors.Errorf("Couldn't configure Authenticator: %v", err) - } - return authenticator, nil - default: - return nil, errors.Errorf("Invalid option for Authenticator named %s", key) - } -} - -// NewAuthorizer returns a new Authorizer -func NewAuthorizer(authorizerPlugin *ast.ObjectItem) (authorization.Authorizer, error) { - key, data, _ := getPluginConfig(authorizerPlugin) - - switch key { - case "RBAC": - // check if data is defined - if data == nil { - return nil, errors.New("RBAC Authorizer plugin ('config > plugins > Authorizer RBAC > plugin_data') not populated") - } - fmt.Printf("Authorizer RBAC Plugin Data: %+v\n", data) - - // decode config to struct - var config pluginAuthorizerRBAC - if err := hcl.DecodeObject(&config, data); err != nil { - return nil, errors.Errorf("Couldn't parse Authorizer config: %v", err) - } - - // 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 - if role.Name == "" { - fmt.Println("WARNING: using the empty string for an API enables access to all authenticated users") - } - } - 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, apiV1Mapping) - if err != nil { - return nil, errors.Errorf("Couldn't configure Authorizer: %v", err) - } - return authorizer, nil - default: - return nil, errors.Errorf("Invalid option for Authorizer named %s", key) - } -} - -func (s *Server) VerifyConfiguration() error { - if s.TornjakConfig == nil { - return errors.New("config not given") - } - - /* Verify server */ - if s.TornjakConfig.Server == nil { // must be defined - return errors.New("'config > server' field not defined") - } - if s.TornjakConfig.Server.SPIRESocket == "" { - return errors.New("'config > server > spire_socket_path' field not defined") - } - - /* Verify Plugins */ - if s.TornjakConfig.Plugins == nil { - return errors.New("'config > plugins' field not defined") - } - return nil -} - -func (s *Server) ConfigureDefaults() error { - // no authorization is a default - s.Authenticator = authenticator.NewNullAuthenticator() - s.Authorizer = authorization.NewNullAuthorizer() - return nil -} - -func (s *Server) Configure() error { - // Verify Config - err := s.VerifyConfiguration() - if err != nil { - return errors.Errorf("Tornjak Config error: %v", err) - } - - /* Configure Server */ - serverConfig := s.TornjakConfig.Server - s.SpireServerAddr = serverConfig.SPIRESocket // for convenience - - /* Configure Plugins */ - // configure defaults for optional plugins, reconfigured if given - // TODO maybe we should not have this step at all - // This is a temporary work around for optional plugin configs - err = s.ConfigureDefaults() - if err != nil { - return errors.Errorf("Tornjak Config error: %v", err) - } - - pluginConfigs := *s.TornjakConfig.Plugins - pluginList, ok := pluginConfigs.(*ast.ObjectList) - if !ok { - return fmt.Errorf("expected plugins node type %T but got %T", pluginList, pluginConfigs) - } - - // iterate over plugin list - - for _, pluginObject := range pluginList.Items { - if len(pluginObject.Keys) != 2 { - return fmt.Errorf("plugin item expected to have two keys (type then name)") - } - - pluginType, err := stringFromToken(pluginObject.Keys[0].Token) - if err != nil { - return fmt.Errorf("invalid plugin type key %q: %w", pluginObject.Keys[0].Token.Text, err) - } - - // create plugin component based on type - switch pluginType { - // configure datastore - case "DataStore": - s.Db, err = NewAgentsDB(pluginObject) - if err != nil { - return errors.Errorf("Cannot configure datastore plugin: %v", err) - } - // configure Authenticator - case "Authenticator": - s.Authenticator, err = NewAuthenticator(pluginObject) - if err != nil { - return errors.Errorf("Cannot configure Authenticator plugin: %v", err) - } - // configure Authorizer - case "Authorizer": - s.Authorizer, err = NewAuthorizer(pluginObject) - if err != nil { - return errors.Errorf("Cannot configure Authorizer plugin: %v", err) - } - } - // TODO Handle when multiple plugins configured - } - - return nil -} - -func (s *Server) tornjakSelectorsList(w http.ResponseWriter, r *http.Request) { - 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() - var input ListSelectorsRequest - if n == 0 { - input = ListSelectorsRequest{} - } 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.ListSelectors(input) - if err != nil { - emsg := fmt.Sprintf("Error: %v", err.Error()) - retError(w, emsg, http.StatusBadRequest) - 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) tornjakPluginDefine(w http.ResponseWriter, r *http.Request) { - 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() - var input RegisterSelectorRequest - if n == 0 { - input = RegisterSelectorRequest{} - } 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 - } - } - err = s.DefineSelectors(input) - if err != nil { - emsg := fmt.Sprintf("Error: %v", err.Error()) - retError(w, emsg, http.StatusBadRequest) - return - } - cors(w, r) - _, err = w.Write([]byte("SUCCESS")) - if err != nil { - emsg := fmt.Sprintf("Error: %v", err.Error()) - retError(w, emsg, http.StatusBadRequest) - return - } -} - -func (s *Server) tornjakAgentsList(w http.ResponseWriter, r *http.Request) { - 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() - var input ListAgentMetadataRequest - if n == 0 { - input = ListAgentMetadataRequest{} - } 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.ListAgentMetadata(input) - if err != nil { - emsg := fmt.Sprintf("Error: %v", err.Error()) - retError(w, emsg, http.StatusBadRequest) - 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 - } -} - -/********* CLUSTER *********/ - -func (s *Server) clusterList(w http.ResponseWriter, r *http.Request) { - var input ListClustersRequest - 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 = ListClustersRequest{} - } 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.ListClusters(input) - if err != nil { - emsg := fmt.Sprintf("Error: %v", err.Error()) - retError(w, emsg, http.StatusBadRequest) - 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) clusterCreate(w http.ResponseWriter, r *http.Request) { - 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() - var input RegisterClusterRequest - if n == 0 { - input = RegisterClusterRequest{} - } 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 - } - } - err = s.DefineCluster(input) - if err != nil { - emsg := fmt.Sprintf("Error: %v", err.Error()) - retError(w, emsg, http.StatusBadRequest) - return - } - cors(w, r) - _, err = w.Write([]byte("SUCCESS")) - if err != nil { - emsg := fmt.Sprintf("Error: %v", err.Error()) - retError(w, emsg, http.StatusBadRequest) - return - } -} - -func (s *Server) clusterEdit(w http.ResponseWriter, r *http.Request) { - 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() - var input EditClusterRequest - if n == 0 { - input = EditClusterRequest{} - } 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 - } - } - err = s.EditCluster(input) - if err != nil { - emsg := fmt.Sprintf("Error: %v", err.Error()) - retError(w, emsg, http.StatusBadRequest) - return - } - cors(w, r) - _, err = w.Write([]byte("SUCCESS")) - if err != nil { - emsg := fmt.Sprintf("Error: %v", err.Error()) - retError(w, emsg, http.StatusBadRequest) - return - } -} - -func (s *Server) clusterDelete(w http.ResponseWriter, r *http.Request) { - 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() - var input DeleteClusterRequest - if n == 0 { - input = DeleteClusterRequest{} - } 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 - } - } - err = s.DeleteCluster(input) - if err != nil { - emsg := fmt.Sprintf("Error: %v", err.Error()) - retError(w, emsg, http.StatusBadRequest) - return - } - cors(w, r) - _, err = w.Write([]byte("SUCCESS")) - if err != nil { - emsg := fmt.Sprintf("Error: %v", err.Error()) - retError(w, emsg, http.StatusBadRequest) - return - } - -} - -/********* END CLUSTER *********/ From 09aeeff2c566fd9d29b44410308adf78adf1c5eb Mon Sep 17 00:00:00 2001 From: Maia Iyer Date: Mon, 30 Sep 2024 16:37:23 -0400 Subject: [PATCH 3/4] Fix CodeQL check Signed-off-by: Maia Iyer --- api/agent/server.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/api/agent/server.go b/api/agent/server.go index 3ca81726..46db34bc 100644 --- a/api/agent/server.go +++ b/api/agent/server.go @@ -144,20 +144,18 @@ type spaHandler struct { // file located at the index path on the SPA handler will be served. This // is suitable behavior for serving an SPA (single page application). func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + relPath := r.URL.Path // get the absolute path to prevent directory traversal - path, err := filepath.Abs(r.URL.Path) - if err != nil { + absPath, err := filepath.Abs(filepath.Join(h.staticPath, relPath)) + if err != nil || !strings.HasPrefix(absPath, h.staticPath) { // if we failed to get the absolute path respond with a 400 bad request // and stop http.Error(w, err.Error(), http.StatusBadRequest) return } - // prepend the path with the path to the static directory - path = filepath.Join(h.staticPath, path) - // check whether a file exists at the given path - _, err = os.Stat(path) + _, err = os.Stat(absPath) if os.IsNotExist(err) { // file does not exist, serve index.html http.ServeFile(w, r, filepath.Join(h.staticPath, h.indexPath)) From c5d4826f1b10a73cb5aaba029545a14657b80190 Mon Sep 17 00:00:00 2001 From: Maia Iyer Date: Mon, 30 Sep 2024 20:33:44 -0400 Subject: [PATCH 4/4] nit indentation Signed-off-by: Maia Iyer --- api/agent/server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/agent/server.go b/api/agent/server.go index 46db34bc..616a42e6 100644 --- a/api/agent/server.go +++ b/api/agent/server.go @@ -144,7 +144,7 @@ type spaHandler struct { // file located at the index path on the SPA handler will be served. This // is suitable behavior for serving an SPA (single page application). func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - relPath := r.URL.Path + relPath := r.URL.Path // get the absolute path to prevent directory traversal absPath, err := filepath.Abs(filepath.Join(h.staticPath, relPath)) if err != nil || !strings.HasPrefix(absPath, h.staticPath) {