blob: 929656490a0464584eee8a90894cf6eb0796dbe2 [file] [log] [blame]
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"
"errors"
"fmt"
"net/http"
"strconv"
"time"
"github.com/apache/trafficcontrol/lib/go-tc"
"github.com/apache/trafficcontrol/lib/go-tc/tovalidate"
"github.com/apache/trafficcontrol/lib/go-util"
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/auth"
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/dbhelpers"
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/tenant"
validation "github.com/go-ozzo/ozzo-validation"
"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
}