From 16722cd69aabee19e0b107e54910b467ef4de566 Mon Sep 17 00:00:00 2001 From: Jagan Parthiban Date: Tue, 8 Aug 2023 17:59:14 +0530 Subject: [PATCH] Fixes https://github.com/apache/trafficcontrol/issues/7704 --- lib/go-tc/parameters.go | 46 +++ .../dbhelpers/db_helpers.go | 15 + .../parameter/parameters.go | 367 ++++++++++++++++++ .../traffic_ops_golang/routing/routes.go | 8 +- 4 files changed, 432 insertions(+), 4 deletions(-) diff --git a/lib/go-tc/parameters.go b/lib/go-tc/parameters.go index 57cce3206f..0667c0080f 100644 --- a/lib/go-tc/parameters.go +++ b/lib/go-tc/parameters.go @@ -27,6 +27,7 @@ import ( "fmt" "strconv" "strings" + "time" "github.com/apache/trafficcontrol/lib/go-util" @@ -69,6 +70,51 @@ type ParameterNullable struct { Value *string `json:"value" db:"value"` } +// ParametersResponseV5 is an alias for the latest minor version for the major version 5. +type ParametersResponseV5 = ParametersResponseV50 + +// ParametersResponseV50 is the type of the response from Traffic Ops to GET +// requests made to the /parameters and /profiles/name/{{Name}}/parameters +// endpoints of its API. +type ParametersResponseV50 struct { + Response []Parameter `json:"response"` + Alerts +} + +// ParameterV5 is an alias for the latest minor version for the major version 5. +type ParameterV5 = ParameterV50 + +// A ParameterV50 defines some configuration setting (which is usually but +// definitely not always a line in a configuration file) used by some Profile +// or Cache Group. +type ParameterV50 struct { + ConfigFile string `json:"configFile" db:"config_file"` + ID int `json:"id" db:"id"` + LastUpdated time.Time `json:"lastUpdated" db:"last_updated"` + Name string `json:"name" db:"name"` + Profiles json.RawMessage `json:"profiles" db:"profiles"` + Secure bool `json:"secure" db:"secure"` + Value string `json:"value" db:"value"` +} + +// ParameterNullableV5 is an alias for the latest minor version for the major version 5. +type ParameterNullableV5 = ParameterNullableV50 + +// ParameterNullableV50 is exactly like Parameter except that its properties are +// reference values, so they can be nil. +type ParameterNullableV50 struct { + // + // NOTE: the db: struct tags are used for testing to map to their equivalent database column (if there is one) + // + ConfigFile *string `json:"configFile" db:"config_file"` + ID *int `json:"id" db:"id"` + LastUpdated *time.Time `json:"lastUpdated" db:"last_updated"` + Name *string `json:"name" db:"name"` + Profiles json.RawMessage `json:"profiles" db:"profiles"` + Secure *bool `json:"secure" db:"secure"` + Value *string `json:"value" db:"value"` +} + // ProfileParameterByName is a structure that's used to represent a Parameter // in a context where they are associated with some Profile specified by a // client of the Traffic Ops API by its Name. diff --git a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go index c70056279b..b307ee19fa 100644 --- a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go +++ b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go @@ -2263,6 +2263,21 @@ func PhysLocationExists(tx *sql.Tx, id string) (bool, error) { return true, nil } +// ParameterExists confirms whether the Parameter exists, and an error (if one occurs). +func ParameterExists(tx *sql.Tx, id string) (bool, error) { + var count int + if err := tx.QueryRow("SELECT count(name) FROM parameter WHERE id=$1", id).Scan(&count); err != nil { + return false, fmt.Errorf("error getting Parameter info: %w", err) + } + if count == 0 { + return false, nil + } + if count != 1 { + return false, fmt.Errorf("getting Parameter info - expected row count: 1, actual: %d", count) + } + return true, nil +} + // GetCoordinateID obtains coordinateID, and an error (if one occurs) func GetCoordinateID(tx *sql.Tx, id int) (*int, error) { q := `SELECT coordinate FROM cachegroup WHERE id = $1` diff --git a/traffic_ops/traffic_ops_golang/parameter/parameters.go b/traffic_ops/traffic_ops_golang/parameter/parameters.go index 167019fbc3..038e273a5a 100644 --- a/traffic_ops/traffic_ops_golang/parameter/parameters.go +++ b/traffic_ops/traffic_ops_golang/parameter/parameters.go @@ -20,8 +20,13 @@ package parameter */ import ( + "database/sql" + "encoding/json" "errors" + "fmt" + "io" "net/http" + "reflect" "strconv" "time" @@ -251,3 +256,365 @@ func deleteQuery() string { WHERE id=:id` return query } + +func GetParameters(w http.ResponseWriter, r *http.Request) { + var runSecond bool + var maxTime time.Time + inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil) + tx := inf.Tx + if userErr != nil || sysErr != nil { + api.HandleErr(w, r, tx.Tx, errCode, userErr, sysErr) + return + } + defer inf.Close() + + // Query Parameters to Database Query column mappings + queryParamsToQueryCols := map[string]dbhelpers.WhereColumnInfo{ + ConfigFileQueryParam: {Column: "p.config_file"}, + IDQueryParam: {Column: "p.id", Checker: api.IsInt}, + NameQueryParam: {Column: "p.name"}, + SecureQueryParam: {Column: "p.secure", Checker: api.IsBool}, + ValueQueryParam: {Column: "p.value"}, + } + if _, ok := inf.Params["orderby"]; !ok { + inf.Params["orderby"] = "name" + } + where, orderBy, pagination, queryValues, errs := dbhelpers.BuildWhereAndOrderByAndPagination(inf.Params, queryParamsToQueryCols) + if len(errs) > 0 { + api.HandleErr(w, r, tx.Tx, http.StatusBadRequest, util.JoinErrs(errs), nil) + return + } + + if inf.Config.UseIMS { + runSecond, maxTime = ims.TryIfModifiedSinceQuery(tx, r.Header, queryValues, selectMaxLastUpdatedQuery(where)) + if !runSecond { + log.Debugln("IMS HIT") + api.AddLastModifiedHdr(w, maxTime) + w.WriteHeader(http.StatusNotModified) + return + } + log.Debugln("IMS MISS") + } else { + log.Debugln("Non IMS request") + } + + query := selectQuery() + where + ParametersGroupBy() + orderBy + pagination + rows, err := tx.NamedQuery(query, queryValues) + if err != nil { + api.HandleErr(w, r, tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("Parameter read: error getting Parameter(s): %w", err)) + return + } + defer log.Close(rows, "unable to close DB connection") + + params := tc.ParameterNullableV5{} + paramsList := []tc.ParameterNullableV5{} + for rows.Next() { + if err = rows.Scan(¶ms.ConfigFile, ¶ms.ID, ¶ms.LastUpdated, ¶ms.Name, ¶ms.Value, ¶ms.Secure, ¶ms.Profiles); err != nil { + api.HandleErr(w, r, tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("error getting parameter(s): %w", err)) + return + } + if params.Secure != nil && *params.Secure { + if inf.Version.Major >= 4 && + inf.Config.RoleBasedPermissions && + !inf.User.Can("PARAMETER-SECURE:READ") { + params.Value = &HiddenField + } else if inf.User.PrivLevel < auth.PrivLevelAdmin { + params.Value = &HiddenField + } + } + + paramsList = append(paramsList, params) + } + + api.WriteResp(w, r, paramsList) + return +} + +func CreateParameter(w http.ResponseWriter, r *http.Request) { + inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil) + if userErr != nil || sysErr != nil { + api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr) + return + } + defer inf.Close() + tx := inf.Tx.Tx + + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "Error reading request body", http.StatusBadRequest) + return + } + defer r.Body.Close() + + var data interface{} + err = json.Unmarshal(body, &data) + if err != nil { + http.Error(w, "Invalid JSON format", http.StatusBadRequest) + return + } + + var params []tc.ParameterV5 + switch reflect.TypeOf(data).Kind() { + case reflect.Slice: + if err := json.Unmarshal(body, ¶ms); err != nil { + http.Error(w, "Error unmarshaling slice", http.StatusBadRequest) + return + } + case reflect.Map: + // Single object, convert to a slice of one + var param tc.ParameterV5 + if err := json.Unmarshal(body, ¶m); err != nil { + http.Error(w, "Error unmarshaling single object", http.StatusBadRequest) + return + } + params = append(params, param) + default: + http.Error(w, "Invalid request format", http.StatusBadRequest) + return + } + + for _, parameter := range params { + readValErr := validateRequestParameter(parameter) + if readValErr != nil { + api.HandleErr(w, r, tx, http.StatusBadRequest, readValErr, nil) + return + } + } + + // check if parameter already exists + for _, parameter := range params { + var count int + err = tx.QueryRow("SELECT count(*) from parameter where name = $1", parameter.Name).Scan(&count) + if err != nil { + api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("error: %w, when checking if parameter with name %s exists", err, parameter.Name)) + return + } + if count == 1 { + api.HandleErr(w, r, tx, http.StatusBadRequest, fmt.Errorf("parameter name '%s' already exists", parameter.Name), nil) + return + } + } + + // create phys_location + for _, parameter := range params { + query := ` + INSERT INTO parameter ( + name, + config_file, + value, + secure + ) VALUES ( + $1, $2, $3, $4 + ) RETURNING id,last_updated + ` + + err = tx.QueryRow( + query, + parameter.Name, + parameter.ConfigFile, + parameter.Value, + parameter.Secure, + ).Scan( + ¶meter.ID, + ¶meter.LastUpdated, + ) + + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + api.HandleErr(w, r, tx, http.StatusInternalServerError, fmt.Errorf("error: %w in parameter with name: %s", err, parameter.Name), nil) + return + } + usrErr, sysErr, code := api.ParseDBError(err) + api.HandleErr(w, r, tx, code, usrErr, sysErr) + return + } + } + alerts := tc.CreateAlerts(tc.SuccessLevel, "All Requested Parameters were created.") + //w.Header().Set("Location", fmt.Sprintf("/api/%d.%d/parameters?name=%s", inf.Version.Major, inf.Version.Minor, parameter.Name)) + api.WriteAlertsObj(w, r, http.StatusCreated, alerts, params) + return +} + +func UpdateParameter(w http.ResponseWriter, r *http.Request) { + inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil) + if userErr != nil || sysErr != nil { + api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr) + return + } + defer inf.Close() + + tx := inf.Tx.Tx + parameter, readValErr := readAndValidateJsonStruct(r) + if readValErr != nil { + api.HandleErr(w, r, tx, http.StatusBadRequest, readValErr, nil) + return + } + + requestedID := inf.Params["id"] + + intRequestId, convErr := strconv.Atoi(requestedID) + if convErr != nil { + api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, fmt.Errorf("parameter update error: %w, while converting from string to int", convErr), nil) + } + // check if the entity was already updated + userErr, sysErr, errCode = api.CheckIfUnModified(r.Header, inf.Tx, intRequestId, "parameter") + if userErr != nil || sysErr != nil { + api.HandleErr(w, r, tx, errCode, userErr, sysErr) + return + } + + //update name and description of a phys_location + query := ` + UPDATE parameter p + SET + config_file = $1, + name = $2, + value = $3, + secure = $4 + WHERE + p.id = $5 + RETURNING + p.id, + p.last_updated +` + + err := tx.QueryRow( + query, + parameter.ConfigFile, + parameter.Name, + parameter.Value, + parameter.Secure, + requestedID, + ).Scan( + ¶meter.ID, + ¶meter.LastUpdated, + ) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + api.HandleErr(w, r, tx, http.StatusBadRequest, fmt.Errorf("parameter with ID: %v not found", parameter.ID), nil) + return + } + usrErr, sysErr, code := api.ParseDBError(err) + api.HandleErr(w, r, tx, code, usrErr, sysErr) + return + } + alerts := tc.CreateAlerts(tc.SuccessLevel, "parameter was updated") + api.WriteAlertsObj(w, r, http.StatusOK, alerts, parameter) + return +} + +func DeleteParameter(w http.ResponseWriter, r *http.Request) { + inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil) + tx := inf.Tx.Tx + if userErr != nil || sysErr != nil { + api.HandleErr(w, r, tx, errCode, userErr, sysErr) + return + } + defer inf.Close() + + id := inf.Params["id"] + if id == "" { + api.HandleErr(w, r, tx, http.StatusBadRequest, fmt.Errorf("couldn't delete Parameter. Invalid ID. Id Cannot be empty for Delete Operation"), nil) + return + } + exists, err := dbhelpers.ParameterExists(tx, id) + if err != nil { + api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err) + return + } + if !exists { + api.HandleErr(w, r, tx, http.StatusNotFound, fmt.Errorf("no Parameter exists by id: %s", id), nil) + return + } + + assignedProfile := 0 + if err := inf.Tx.Get(&assignedProfile, "SELECT count(profile) FROM profile_parameter pp WHERE pp.parameter=$1", id); err != nil { + api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("parameter delete error, could not count assigned profiles: %w", err)) + return + } else if assignedProfile != 0 { + api.HandleErr(w, r, tx, http.StatusBadRequest, fmt.Errorf("can not delete a parameter with %d assigned profile", assignedProfile), nil) + return + } + + res, err := tx.Exec("DELETE FROM parameter AS p WHERE p.id=$1", id) + if err != nil { + api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err) + return + } + rowsAffected, err := res.RowsAffected() + if err != nil { + api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("error determining rows affected for delete parameter: %w", err)) + return + } + if rowsAffected == 0 { + api.HandleErr(w, r, tx, http.StatusInternalServerError, fmt.Errorf("no rows deleted for parameter"), nil) + return + } + alerts := tc.CreateAlerts(tc.SuccessLevel, "parameter"+ + " was deleted.") + api.WriteAlerts(w, r, http.StatusOK, alerts) + return +} + +// selectMaxLastUpdatedQuery used for TryIfModifiedSinceQuery() +func selectMaxLastUpdatedQuery(where string) string { + return ` +SELECT max(t) from ( + SELECT max(p.last_updated) as t FROM parameter p + LEFT JOIN profile_parameter pp ON p.id = pp.parameter + LEFT JOIN profile pr ON pp.profile = pr.id ` + where + + ` UNION ALL + select max(last_updated) as t from last_deleted l where l.table_name='parameter' +) as res` +} + +func readAndValidateJsonStruct(r *http.Request) (tc.ParameterV5, error) { + var parameter tc.ParameterV5 + if err := json.NewDecoder(r.Body).Decode(¶meter); err != nil { + userErr := fmt.Errorf("error decoding POST request body into ParameterV5 struct %w", err) + return parameter, userErr + } + + // validate JSON body + errs := make(map[string]error) + + errs[NameQueryParam] = validation.Validate(parameter.Name, validation.Required) + errs[ConfigFileQueryParam] = validation.Validate(parameter.ConfigFile, validation.Required) + + if parameter.ConfigFile == atscfg.ParentConfigFileName && parameter.Name == atscfg.ParentConfigCacheParamWeight { + key := atscfg.ParentConfigFileName + " " + atscfg.ParentConfigCacheParamWeight + errs[key] = validation.Validate(parameter.Value, tovalidate.StringIsValidFloat()) + } + + if len(errs) > 0 { + var errorSlice []error + for _, err := range errs { + errorSlice = append(errorSlice, err) + } + userErr := util.JoinErrs(errorSlice) + return parameter, userErr + } + return parameter, nil +} + +func validateRequestParameter(parameter tc.ParameterV5) error { + errs := make(map[string]error) + + errs[NameQueryParam] = validation.Validate(parameter.Name, validation.Required) + errs[ConfigFileQueryParam] = validation.Validate(parameter.ConfigFile, validation.Required) + + if parameter.ConfigFile == atscfg.ParentConfigFileName && parameter.Name == atscfg.ParentConfigCacheParamWeight { + key := atscfg.ParentConfigFileName + " " + atscfg.ParentConfigCacheParamWeight + errs[key] = validation.Validate(parameter.Value, tovalidate.StringIsValidFloat()) + } + + if len(errs) > 0 { + var errorSlice []error + for _, err := range errs { + errorSlice = append(errorSlice, err) + } + userErr := util.JoinErrs(errorSlice) + return userErr + } + return nil +} diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go b/traffic_ops/traffic_ops_golang/routing/routes.go index f62a77db47..6dc9aec207 100644 --- a/traffic_ops/traffic_ops_golang/routing/routes.go +++ b/traffic_ops/traffic_ops_golang/routing/routes.go @@ -250,10 +250,10 @@ func Routes(d ServerData) ([]Route, http.Handler, error) { {Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodPut, Path: `user/current/?$`, Handler: user.ReplaceCurrentV4, RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: nil, Authenticated: Authenticated, Middlewares: nil, ID: 42031}, //Parameter: CRUD - {Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodGet, Path: `parameters/?$`, Handler: api.ReadHandler(¶meter.TOParameter{}), RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"PARAMETER:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 421255429231}, - {Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodPut, Path: `parameters/{id}$`, Handler: api.UpdateHandler(¶meter.TOParameter{}), RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"PARAMETER:UPDATE", "PARAMETER:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 487393611531}, - {Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodPost, Path: `parameters/?$`, Handler: api.CreateHandler(¶meter.TOParameter{}), RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"PARAMETER:CREATE", "PARAMETER:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 466951085931}, - {Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodDelete, Path: `parameters/{id}$`, Handler: api.DeleteHandler(¶meter.TOParameter{}), RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"PARAMETER:DELETE", "PARAMETER:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 42627711831}, + {Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodGet, Path: `parameters/?$`, Handler: parameter.GetParameters, RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"PARAMETER:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 421255429231}, + {Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodPut, Path: `parameters/{id}$`, Handler: parameter.UpdateParameter, RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"PARAMETER:UPDATE", "PARAMETER:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 487393611531}, + {Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodPost, Path: `parameters/?$`, Handler: parameter.CreateParameter, RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"PARAMETER:CREATE", "PARAMETER:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 466951085931}, + {Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodDelete, Path: `parameters/{id}$`, Handler: parameter.DeleteParameter, RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"PARAMETER:DELETE", "PARAMETER:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 42627711831}, //Phys_Location: CRUD {Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodGet, Path: `phys_locations/?$`, Handler: physlocation.GetPhysLocation, RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"PHYSICAL-LOCATION:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 42040518231},