traffic_ops/traffic_ops_golang/apitenant/tenant.go (627 lines of code) (raw):
package apitenant
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
// tenant.go defines the TOTenant object and methods/functions required for the api/.../tenants endpoints
import (
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"time"
"github.com/apache/trafficcontrol/v8/lib/go-log"
"github.com/apache/trafficcontrol/v8/lib/go-tc"
"github.com/apache/trafficcontrol/v8/lib/go-tc/tovalidate"
"github.com/apache/trafficcontrol/v8/lib/go-util"
"github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/api"
"github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/auth"
"github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/dbhelpers"
"github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/tenant"
"github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/util/ims"
validation "github.com/go-ozzo/ozzo-validation"
"github.com/jmoiron/sqlx"
"github.com/lib/pq"
)
const rootName = `root`
// TOTenant provides a local type against which to define methods
type TOTenant struct {
api.APIInfoImpl `json:"-"`
tc.TenantNullable
}
func (ten *TOTenant) GetLastUpdated() (*time.Time, bool, error) {
return api.GetLastUpdated(ten.APIInfo().Tx, *ten.ID, "tenant")
}
func (ten *TOTenant) SetLastUpdated(t tc.TimeNoMod) { ten.LastUpdated = &t }
func (ten *TOTenant) InsertQuery() string { return insertQuery() }
func (ten *TOTenant) SelectMaxLastUpdatedQuery(where, orderBy, pagination, tableName string) string {
return `SELECT max(t) from (
SELECT max(last_updated) as t from ` + tableName + ` q ` + where + orderBy + pagination +
` UNION ALL
select max(last_updated) as t from last_deleted l where l.table_name='` + tableName + `') as res`
}
func (ten *TOTenant) NewReadObj() interface{} { return &tc.TenantNullable{} }
func (ten *TOTenant) SelectQuery() string {
return selectQuery(ten.APIInfo().User.TenantID)
}
func (ten *TOTenant) ParamColumns() map[string]dbhelpers.WhereColumnInfo {
return map[string]dbhelpers.WhereColumnInfo{
"active": dbhelpers.WhereColumnInfo{Column: "q.active", Checker: nil},
"id": dbhelpers.WhereColumnInfo{Column: "q.id", Checker: api.IsInt},
"name": dbhelpers.WhereColumnInfo{Column: "q.name", Checker: nil},
"parent_id": dbhelpers.WhereColumnInfo{Column: "q.parent_id", Checker: api.IsInt},
"parent_name": dbhelpers.WhereColumnInfo{Column: "p.name", Checker: nil},
}
}
func (ten *TOTenant) UpdateQuery() string { return updateQuery() }
// GetID wraps the ID member with null checking
// Part of the Identifier interface
func (ten TOTenant) GetID() (int, bool) {
if ten.ID == nil {
return 0, false
}
return *ten.ID, true
}
// GetKeyFieldsInfo identifies types of the key fields
func (ten TOTenant) GetKeyFieldsInfo() []api.KeyFieldInfo {
return []api.KeyFieldInfo{{Field: "id", Func: api.GetIntKey}}
}
// GetKeys returns values of keys
func (ten TOTenant) GetKeys() (map[string]interface{}, bool) {
var id int
if ten.ID != nil {
id = *ten.ID
}
return map[string]interface{}{"id": id}, true
}
// GetAuditName returns a unique identifier
// Part of the Identifier interface
func (ten TOTenant) GetAuditName() string {
if ten.Name != nil {
return *ten.Name
}
id, _ := ten.GetID()
return strconv.Itoa(id)
}
// GetType returns the name of the type for messages
// Part of the Identifier interface
func (ten TOTenant) GetType() string {
return "tenant"
}
// SetKeys allows CreateHandler to assign id once object is created.
// Part of the Identifier interface
func (ten *TOTenant) SetKeys(keys map[string]interface{}) {
i, _ := keys["id"].(int) //this utilizes the non panicking type assertion, if the thrown away ok variable is false i will be the zero of the type, 0 here.
ten.ID = &i
}
// Validate fulfills the api.Validator interface.
func (ten TOTenant) Validate() (error, error) {
errs := validation.Errors{
"name": validation.Validate(ten.Name, validation.Required),
"active": validation.Validate(ten.Active), // only validate it's boolean
"parentId": validation.Validate(ten.ParentID, validation.Required, validation.Min(1)),
"parentName": nil,
}
return util.JoinErrs(tovalidate.ToErrors(errs)), nil
}
func (ten *TOTenant) Create() (error, error, int) { return api.GenericCreate(ten) }
func (ten *TOTenant) Read(h http.Header, useIMS bool) ([]interface{}, error, error, int, *time.Time) {
if ten.APIInfo().User.TenantID == auth.TenantIDInvalid {
return nil, nil, nil, http.StatusOK, nil
}
api.DefaultSort(ten.APIInfo(), "name")
tenants, userErr, sysErr, errCode, maxTime := api.GenericRead(h, ten, useIMS)
if userErr != nil || sysErr != nil {
return nil, userErr, sysErr, errCode, nil
}
tenantNames := map[int]*string{}
for _, it := range tenants {
t := it.(*tc.TenantNullable)
tenantNames[*t.ID] = t.Name
}
for _, it := range tenants {
t := it.(*tc.TenantNullable)
if t.ParentID == nil || tenantNames[*t.ParentID] == nil {
// root tenant has no parent
continue
}
p := *tenantNames[*t.ParentID]
t.ParentName = &p // copy
}
return tenants, nil, nil, errCode, maxTime
}
// IsTenantAuthorized implements the Tenantable interface for TOTenant
// returns true if the user has access on this tenant and on the ParentID if changed.
func (ten *TOTenant) IsTenantAuthorized(user *auth.CurrentUser) (bool, error) {
var ok = false
var err error
if ten == nil {
// should never happen
return ok, err
}
if ten.ID != nil && *ten.ID != 0 {
// modifying an existing tenant
ok, err = tenant.IsResourceAuthorizedToUserTx(*ten.ID, user, ten.APIInfo().Tx.Tx)
if !ok || err != nil {
return ok, err
}
if ten.ParentID == nil || *ten.ParentID == 0 {
// not changing parent
return ok, err
}
// get current parentID to check if it's being changed
var parentID int
tx := ten.APIInfo().Tx.Tx
// If it's the root tenant, don't check for parent
if ten.Name != nil && *ten.Name != rootName {
err = tx.QueryRow(`SELECT parent_id FROM tenant WHERE id = ` + strconv.Itoa(*ten.ID)).Scan(&parentID)
if err != nil {
return false, err
}
if parentID == *ten.ParentID {
// parent not being changed
return ok, err
}
}
}
// creating new tenant -- must specify a parent
if ten.ParentID == nil || *ten.ParentID == 0 {
return false, err
}
return tenant.IsResourceAuthorizedToUserTx(*ten.ParentID, user, ten.APIInfo().Tx.Tx)
}
// Update wraps tenant validation and the generic API Update call into a single call.
func (ten *TOTenant) Update(h http.Header) (error, error, int) {
userErr, sysErr, statusCode := ten.isUpdatable()
if userErr != nil || sysErr != nil {
return userErr, sysErr, statusCode
}
return api.GenericUpdate(h, ten)
}
// isUpdatable peforms validation on the fields for the Tenant, such as ensuring
// the tenant cannot be modified if it is root, or that it cannot convert its own child
// to its own parent. This is different than the basic validation rules performed in
// Validate() as it pertains to specific business logic, not generic API rules.
func (ten *TOTenant) isUpdatable() (error, error, int) {
if ten.Name != nil && *ten.Name == rootName {
return errors.New("trying to change the root tenant, which is immutable"), nil, http.StatusBadRequest
}
// Perform SelectQuery
vals := []tc.TenantNullable{}
query := selectQuery(*ten.ID)
rows, err := ten.APIInfo().Tx.Queryx(query)
if err != nil {
return nil, errors.New("querying " + ten.GetType() + ": " + err.Error()), http.StatusInternalServerError
}
defer rows.Close()
for rows.Next() {
var v tc.TenantNullable
if err = rows.StructScan(&v); err != nil {
return nil, errors.New("scanning " + ten.GetType() + ": " + err.Error()), http.StatusInternalServerError
}
vals = append(vals, v)
}
// Ensure the new desired ParentID does not exist in the susequent list of Children
for _, val := range vals {
if *ten.ParentID == *val.ID {
return errors.New("trying to set existing child as new parent"), nil, http.StatusBadRequest
}
}
return nil, nil, http.StatusOK
}
func (ten *TOTenant) Delete() (error, error, int) {
result, err := ten.APIInfo().Tx.NamedExec(deleteQuery(), ten)
if err != nil {
return parseDeleteErr(err, *ten.ID, ten.APIInfo().Tx.Tx) // this is why we can't use api.GenericDelete
}
if rowsAffected, err := result.RowsAffected(); err != nil {
return nil, errors.New("deleting " + ten.GetType() + ": getting rows affected: " + err.Error()), http.StatusInternalServerError
} else if rowsAffected < 1 {
return errors.New("no " + ten.GetType() + " with that id found"), nil, http.StatusNotFound
} else if rowsAffected > 1 {
return nil, fmt.Errorf(ten.GetType()+" delete affected too many rows: %d", rowsAffected), http.StatusInternalServerError
}
return nil, nil, http.StatusOK
}
// parseDeleteErr takes the tenant delete error, and returns the appropriate user error, system error, and http status code.
func parseDeleteErr(err error, id int, tx *sql.Tx) (error, error, int) {
pqErr, ok := err.(*pq.Error)
if !ok {
return nil, errors.New("deleting tenant: " + err.Error()), http.StatusInternalServerError
}
// TODO fix this to check for other Postgres errors besides key violations
existing := ""
switch pqErr.Table {
case "tenant":
existing = "child tenants"
case "tm_user":
existing = "users"
case "deliveryservice":
existing = "deliveryservices"
case "origin":
existing = "origins"
default:
existing = pqErr.Table
}
return errors.New("Tenant '" + strconv.Itoa(id) + "' has " + existing + ". Please update these " + existing + " and retry."), nil, http.StatusBadRequest
}
// selectQuery returns a query on the tenant table that limits to tenants within the realm of the tenantID. It's intended
// to be extensible by adding WHERE/ORDERBY/etc clauses at the end to further refine the query.
func selectQuery(tenantID int) string {
query := `
WITH RECURSIVE q AS (
SELECT id, name, active, parent_id, last_updated FROM tenant WHERE id = ` + strconv.Itoa(tenantID) + `
UNION SELECT t.id, t.name, t.active, t.parent_id, t.last_updated FROM tenant t JOIN q ON q.id = t.parent_id)
SELECT q.id AS id, q.name AS name, q.active AS active, q.parent_id AS parent_id, q.last_updated AS last_updated,
p.name AS parent_name FROM q LEFT JOIN tenant p ON q.parent_id = p.id
`
return query
}
func updateQuery() string {
query := `UPDATE
tenant SET
active=:active,
name=:name,
parent_id=:parent_id
WHERE id=:id RETURNING last_updated`
return query
}
func insertQuery() string {
query := `INSERT INTO tenant (
name,
active,
parent_id
) VALUES (
:name,
:active,
:parent_id
) RETURNING id,last_updated`
return query
}
func deleteQuery() string {
query := `DELETE FROM tenant
WHERE id=:id`
return query
}
func selectMaxLastUpdatedQuery(where string) string {
tableName := "tenant"
return `SELECT max(t) from (
SELECT max(last_updated) as t from ` + tableName + ` q ` + where +
` UNION ALL
select max(last_updated) as t from last_deleted l where l.table_name='` + tableName + `') as res`
}
// CreateTenant [Version : V5] function Process the *http.Request and creates new tenant
func CreateTenant(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
defer r.Body.Close()
tenant, readValErr := readAndValidateJsonStruct(r)
if readValErr != nil {
api.HandleErr(w, r, tx, http.StatusBadRequest, readValErr, nil)
return
}
// Check if tenant is tenable
authorized, err := isTenantAuthorizedV5(&tenant, inf.User, tx)
if err != nil {
api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("checking tenant authorized: "+err.Error()))
return
}
if !authorized {
api.HandleErr(w, r, inf.Tx.Tx, http.StatusForbidden, errors.New("not authorized on this tenant"), nil)
return
}
resultRows, err := inf.Tx.NamedQuery(insertQuery(), tenant)
if err != nil {
userErr, sysErr, errCode = api.ParseDBError(err)
api.HandleErr(w, r, tx, errCode, userErr, sysErr)
return
}
defer resultRows.Close()
var id int
lastUpdated := time.Time{}
rowsAffected := 0
for resultRows.Next() {
rowsAffected++
if err := resultRows.Scan(&id, &lastUpdated); err != nil {
api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("tenant create scanning: "+err.Error()))
return
}
}
if rowsAffected == 0 {
api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("tenant create: no tenant was inserted, no id was returned"))
return
} else if rowsAffected > 1 {
api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("too many ids returned from tenant insert"))
return
}
tenant.ID = &id
tenant.LastUpdated = &lastUpdated
alerts := tc.CreateAlerts(tc.SuccessLevel, "tenant was created.")
api.WriteAlertsObj(w, r, http.StatusOK, alerts, tenant)
changeLogMsg := fmt.Sprintf("TENANT: %s, ID: %d, ACTION: Created tenant", *tenant.Name, *tenant.ID)
api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
return
}
// GetTenant [Version : V5] function Process the *http.Request and writes the response. It uses getTenant function.
func GetTenant(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()
user, _ := auth.GetCurrentUser(r.Context())
tenantID := user.TenantID
if tenantID == auth.TenantIDInvalid {
return
}
code := http.StatusOK
useIMS := false
config, e := api.GetConfig(r.Context())
if e == nil && config != nil {
useIMS = config.UseIMS
} else {
log.Warnf("Couldn't get config %v", e)
}
var maxTime *time.Time
var usrErr error
var syErr error
var tenants []tc.TenantV5
api.DefaultSort(inf, "name")
tenants, usrErr, syErr, code, maxTime = getTenants(inf.Tx, inf.Params, useIMS, r.Header, tenantID)
if usrErr != nil {
api.HandleErr(w, r, tx, code, fmt.Errorf("read tenant: get tenant: "+usrErr.Error()), nil)
}
if syErr != nil {
api.HandleErr(w, r, tx, code, nil, fmt.Errorf("read tenant: get tenant: "+syErr.Error()))
}
if maxTime != nil && api.SetLastModifiedHeader(r, useIMS) {
api.AddLastModifiedHdr(w, *maxTime)
w.WriteHeader(http.StatusNotModified)
return
}
tenantNames := map[int]*string{}
for _, it := range tenants {
tenantNames[*it.ID] = it.Name
}
for _, it := range tenants {
if it.ParentID == nil || tenantNames[*it.ParentID] == nil {
// root tenant has no parent
continue
}
p := *tenantNames[*it.ParentID]
it.ParentName = &p // copy
}
api.WriteResp(w, r, tenants)
}
func getTenants(tx *sqlx.Tx, params map[string]string, useIMS bool, header http.Header, id int) ([]tc.TenantV5, error, error, int, *time.Time) {
tenants := make([]tc.TenantV5, 0)
code := http.StatusOK
var maxTime time.Time
var runSecond bool
// Query Parameters to Database Query column mappings
queryParamsToQueryCols := map[string]dbhelpers.WhereColumnInfo{
"active": {Column: "q.active", Checker: nil},
"name": {Column: "q.name", Checker: nil},
"parent_name": {Column: "p.name", Checker: nil},
"id": {Column: "q.id", Checker: api.IsInt},
"parent_id": {Column: "q.parent_id", Checker: api.IsInt},
}
if _, ok := params["orderby"]; !ok {
params["orderby"] = "name"
}
where, orderBy, pagination, queryValues, errs := dbhelpers.BuildWhereAndOrderByAndPagination(params, queryParamsToQueryCols)
if len(errs) > 0 {
return nil, util.JoinErrs(errs), nil, http.StatusBadRequest, nil
}
if useIMS {
runSecond, maxTime = ims.TryIfModifiedSinceQuery(tx, header, queryValues, selectMaxLastUpdatedQuery(where))
if !runSecond {
log.Debugln("IMS HIT")
code = http.StatusNotModified
return tenants, nil, nil, code, &maxTime
}
log.Debugln("IMS MISS")
} else {
log.Debugln("Non IMS request")
}
// Case where we need to run the second query
query := selectQuery(id) + where + orderBy + pagination
rows, err := tx.NamedQuery(query, queryValues)
if err != nil {
return nil, nil, err, http.StatusInternalServerError, nil
}
defer rows.Close()
for rows.Next() {
var t tc.TenantV5
if err = rows.Scan(
&t.ID,
&t.Name,
&t.Active,
&t.ParentID,
&t.LastUpdated,
&t.ParentName,
); err != nil {
return nil, nil, err, http.StatusInternalServerError, nil
}
tenants = append(tenants, t)
}
return tenants, nil, nil, http.StatusOK, nil
}
// UpdateTenant [Version : V5] function Process the *http.Request and updates the tenant.
func UpdateTenant(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
defer r.Body.Close()
var tenant tc.TenantV5
tenant, readValErr := readAndValidateJsonStruct(r)
if readValErr != nil {
api.HandleErr(w, r, tx, http.StatusBadRequest, readValErr, nil)
return
}
if id, ok := inf.Params["id"]; !ok {
api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("missing key: id"), nil)
return
} else {
idNum, err := strconv.Atoi(id)
if err != nil {
api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("couldn't convert ID into a numeric value: "+err.Error()), nil)
return
}
tenant.ID = &idNum
existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, idNum, "tenant")
if err == nil && found == false {
api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no tenant found with this id"), nil)
return
}
if err != nil {
api.HandleErr(w, r, tx, http.StatusNotFound, nil, err)
return
}
if !api.IsUnmodified(r.Header, *existingLastUpdated) {
api.HandleErr(w, r, tx, http.StatusPreconditionFailed, api.ResourceModifiedError, nil)
return
}
// Check if tenant is tenable
authorized, err := isTenantAuthorizedV5(&tenant, inf.User, tx)
if err != nil {
api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("checking tenant authorized: "+err.Error()))
return
}
if !authorized {
api.HandleErr(w, r, inf.Tx.Tx, http.StatusForbidden, errors.New("not authorized on this tenant"), nil)
return
}
//Check if tenant is updatable
userErr, sysErr, statusCode := isUpdatableV5(&tenant, inf.Tx)
if userErr != nil || sysErr != nil {
api.HandleErr(w, r, inf.Tx.Tx, statusCode, userErr, sysErr)
return
}
rows, err := inf.Tx.NamedQuery(updateQuery(), tenant)
if err != nil {
userErr, sysErr, errCode = api.ParseDBError(err)
api.HandleErr(w, r, tx, errCode, userErr, sysErr)
return
}
defer rows.Close()
if !rows.Next() {
api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no tenant found with this id"), nil)
return
}
lastUpdated := time.Time{}
if err := rows.Scan(&lastUpdated); err != nil {
api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("scanning lastUpdated from tenant insert: "+err.Error()))
return
}
tenant.LastUpdated = &lastUpdated
if rows.Next() {
api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("tenant update affected too many rows: >1"))
return
}
alerts := tc.CreateAlerts(tc.SuccessLevel, "tenant was updated.")
api.WriteAlertsObj(w, r, http.StatusOK, alerts, tenant)
changeLogMsg := fmt.Sprintf("TENANT: %s, ID: %d, ACTION: Updated tenant", *tenant.Name, *tenant.ID)
api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
return
}
}
// DeleteTenant [Version : V5] function deletes the tenant passed.
func DeleteTenant(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"]
id, err := strconv.Atoi(ID)
if err != nil {
api.HandleErr(w, r, tx, http.StatusUnprocessableEntity, fmt.Errorf("delete cachegroup: converted to type int: "+err.Error()), nil)
return
}
useIMS := false
var tenantV5 []tc.TenantV5
tenantV5, usrErr, syErr, code, maxTime := getTenants(inf.Tx, inf.Params, useIMS, r.Header, id)
if userErr != nil {
api.HandleErr(w, r, tx, code, fmt.Errorf("delete tenant: get tenant: "+usrErr.Error()), nil)
}
if sysErr != nil {
api.HandleErr(w, r, tx, code, nil, fmt.Errorf("delete tenant: get tenant: "+syErr.Error()))
}
if maxTime != nil && api.SetLastModifiedHeader(r, useIMS) {
api.AddLastModifiedHdr(w, *maxTime)
w.WriteHeader(http.StatusNotModified)
return
}
// Check if tenant is tenable
authorized, err := isTenantAuthorizedV5(&tenantV5[0], inf.User, tx)
if err != nil {
api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("checking tenant authorized: "+err.Error()))
return
}
if !authorized {
api.HandleErr(w, r, inf.Tx.Tx, http.StatusForbidden, errors.New("not authorized on this tenant"), nil)
return
}
res, err := tx.Exec("DELETE FROM tenant AS t WHERE t.ID=$1", id)
if err != nil {
usrErr, syErr, code := parseDeleteErr(err, id, tx)
api.HandleErr(w, r, tx, code, usrErr, syErr)
return
}
rowsAffected, err := res.RowsAffected()
if err != nil {
api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("determining rows affected for delete cachegroup: %w", err))
return
}
if rowsAffected == 0 {
api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("no rows deleted for cachegroup"))
return
}
alertMessage := fmt.Sprint("tenant was deleted.")
alerts := tc.CreateAlerts(tc.SuccessLevel, alertMessage)
api.WriteAlerts(w, r, http.StatusOK, alerts)
changeLogMsg := fmt.Sprintf("TENANT: ID: %d, ACTION: Deleted tenant", id)
api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
return
}
// IsTenantAuthorized implements the Tenantable interface for TOTenant
// returns true if the user has access on this tenant and on the ParentID if changed.
func isTenantAuthorizedV5(tenantV5 *tc.TenantV5, user *auth.CurrentUser, tx *sql.Tx) (bool, error) {
var ok = false
var err error
if tenantV5 == nil {
// should never happen
return ok, err
}
if tenantV5.ID != nil && *tenantV5.ID != 0 {
// modifying an existing tenant
ok, err = tenant.IsResourceAuthorizedToUserTx(*tenantV5.ID, user, tx)
if !ok || err != nil {
return ok, err
}
if tenantV5.ParentID == nil || *tenantV5.ParentID == 0 {
// not changing parent
return ok, err
}
// get current parentID to check if it's being changed
var parentID int
// If it's the root tenant, don't check for parent
if tenantV5.Name != nil && *tenantV5.Name != rootName {
err = tx.QueryRow(`SELECT parent_id FROM tenant WHERE id = ` + strconv.Itoa(*tenantV5.ID)).Scan(&parentID)
if err != nil {
return false, err
}
if parentID == *tenantV5.ParentID {
// parent not being changed
return ok, err
}
}
}
// creating new tenant -- must specify a parent
if tenantV5.ParentID == nil || *tenantV5.ParentID == 0 {
return false, err
}
return tenant.IsResourceAuthorizedToUserTx(*tenantV5.ParentID, user, tx)
}
func isUpdatableV5(tenantV5 *tc.TenantV5, tx *sqlx.Tx) (error, error, int) {
if tenantV5.Name != nil && *tenantV5.Name == rootName {
return errors.New("trying to change the root tenant, which is immutable"), nil, http.StatusBadRequest
}
// Perform SelectQuery
vals := []tc.TenantNullable{}
query := selectQuery(*tenantV5.ID)
rows, err := tx.Queryx(query)
if err != nil {
return nil, errors.New("querying tenant: " + err.Error()), http.StatusInternalServerError
}
defer rows.Close()
for rows.Next() {
var v tc.TenantNullable
if err = rows.StructScan(&v); err != nil {
return nil, errors.New("scanning tenant: " + err.Error()), http.StatusInternalServerError
}
vals = append(vals, v)
}
// Ensure the new desired ParentID does not exist in the susequent list of Children
for _, val := range vals {
if *tenantV5.ParentID == *val.ID {
return errors.New("trying to set existing child as new parent"), nil, http.StatusBadRequest
}
}
return nil, nil, http.StatusOK
}
// readAndValidateJsonStruct populates select missing fields and validates JSON body
func readAndValidateJsonStruct(r *http.Request) (tc.TenantV5, error) {
var ten tc.TenantV5
if err := json.NewDecoder(r.Body).Decode(&ten); err != nil {
userErr := fmt.Errorf("error decoding POST request body into TenantV5 struct %w", err)
return ten, userErr
}
// validate JSON body
rule := validation.NewStringRule(tovalidate.IsAlphanumericUnderscoreDash, "must consist of only alphanumeric, dash, or underscore characters")
errs := tovalidate.ToErrors(validation.Errors{
"name": validation.Validate(ten.Name, validation.Required, rule),
"active": validation.Validate(ten.Active), // only validate it's boolean
"parentId": validation.Validate(ten.ParentID, validation.Required, validation.Min(1)),
"parentName": nil,
})
if len(errs) > 0 {
userErr := util.JoinErrs(errs)
return ten, userErr
}
return ten, nil
}