traffic_ops/traffic_ops_golang/role/roles.go (560 lines of code) (raw):
package role
/*
* 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.
*/
import (
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/http"
"sort"
"strconv"
"strings"
"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/dbhelpers"
"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"
)
type roleError string
func (e roleError) Error() string {
return string(e)
}
const cannotModifyAdminError roleError = "the '" + tc.AdminRoleName + "' Role cannot be deleted or modified"
const isAdminQuery = `SELECT name='` + tc.AdminRoleName + `' FROM public.role WHERE id=$1`
type TORole struct {
api.APIInfoImpl `json:"-"`
tc.Role
LastUpdated *tc.TimeNoMod `json:"-"`
PQCapabilities *pq.StringArray `json:"-" db:"capabilities"`
}
func updateRoleQuery() string {
return `UPDATE
role SET
name=$1,
description=$2
WHERE name=$3 RETURNING last_updated`
}
func (v *TORole) GetLastUpdated() (*time.Time, bool, error) {
return api.GetLastUpdated(v.APIInfo().Tx, *v.ID, "role")
}
func (v *TORole) SelectMaxLastUpdatedQuery(where, orderBy, pagination, tableName string) string {
return `SELECT max(t) from (
SELECT max(last_updated) as t from ` + tableName + ` r ` + where + orderBy + pagination +
` UNION ALL
select max(last_updated) as t from last_deleted l where l.table_name='` + tableName + `') as res`
}
func (v *TORole) SetLastUpdated(t tc.TimeNoMod) { v.LastUpdated = &t }
func (v *TORole) InsertQuery() string { return insertQuery() }
func (v *TORole) NewReadObj() interface{} { return &TORole{} }
func (v *TORole) SelectQuery() string { return selectQuery() }
func (v *TORole) ParamColumns() map[string]dbhelpers.WhereColumnInfo {
return map[string]dbhelpers.WhereColumnInfo{
"name": dbhelpers.WhereColumnInfo{Column: "name"},
"id": dbhelpers.WhereColumnInfo{Column: "id", Checker: api.IsInt},
"privLevel": dbhelpers.WhereColumnInfo{Column: "priv_level", Checker: api.IsInt}}
}
func (v *TORole) UpdateQuery() string { return updateQuery() }
func (v *TORole) DeleteQuery() string { return deleteQuery() }
func (role TORole) GetKeyFieldsInfo() []api.KeyFieldInfo {
return []api.KeyFieldInfo{{Field: "id", Func: api.GetIntKey}}
}
// Implementation of the Identifier, Validator interface functions
func (role TORole) GetKeys() (map[string]interface{}, bool) {
if role.ID == nil {
return map[string]interface{}{"id": 0}, false
}
return map[string]interface{}{"id": *role.ID}, true
}
func (role TORole) GetAuditName() string {
if role.Name != nil {
return *role.Name
}
if role.ID != nil {
return strconv.Itoa(*role.ID)
}
return "0"
}
func (role TORole) GetType() string {
return "role"
}
func (role *TORole) 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.
role.ID = &i
}
// Validate fulfills the api.Validator interface
func (role TORole) Validate() (error, error) {
errs := validation.Errors{
"name": validation.Validate(role.Name, validation.Required),
"description": validation.Validate(role.Description, validation.Required),
"privLevel": validation.Validate(role.PrivLevel, validation.NotNil)}
errsToReturn := tovalidate.ToErrors(errs)
checkCaps := `SELECT cap FROM UNNEST($1::text[]) AS cap WHERE NOT cap = ANY(ARRAY(SELECT c.name FROM capability AS c WHERE c.name = ANY($1)))`
var badCaps []string
if role.ReqInfo.Tx != nil {
err := role.ReqInfo.Tx.Select(&badCaps, checkCaps, pq.Array(role.Capabilities))
if err != nil {
return nil, fmt.Errorf("got error from selecting bad capabilities: %w", err)
}
if len(badCaps) > 0 {
errsToReturn = append(errsToReturn, fmt.Errorf("can not add non-existent capabilities: %v", badCaps))
}
}
return util.JoinErrs(errsToReturn), nil
}
func (role *TORole) Create() (error, error, int) {
if *role.PrivLevel > role.ReqInfo.User.PrivLevel {
return errors.New("can not create a role with a higher priv level than your own"), nil, http.StatusBadRequest
}
if role.Capabilities != nil && *role.Capabilities != nil {
caps := *role.Capabilities
missing := role.ReqInfo.User.MissingPermissions(caps...)
if len(missing) != 0 {
return fmt.Errorf("cannot request more than assigned permissions, current user needs %s permissions", strings.Join(missing, ",")), nil, http.StatusForbidden
}
}
userErr, sysErr, errCode := api.GenericCreate(role)
if userErr != nil || sysErr != nil {
return userErr, sysErr, errCode
}
//after we have role ID we can associate the capabilities:
if role.Capabilities != nil && len(*role.Capabilities) > 0 {
userErr, sysErr, errCode = role.createRoleCapabilityAssociations(role.ReqInfo.Tx)
if userErr != nil || sysErr != nil {
return userErr, sysErr, errCode
}
}
return nil, nil, http.StatusOK
}
func (role *TORole) createRoleCapabilityAssociations(tx *sqlx.Tx) (error, error, int) {
result, err := tx.Exec(associateCapabilities(), role.ID, pq.Array(role.Capabilities))
if err != nil {
return nil, errors.New("creating role capabilities: " + err.Error()), http.StatusInternalServerError
}
if rows, err := result.RowsAffected(); err != nil {
log.Errorf("could not check result after inserting role_capability relations: %v", err)
} else if expected := len(*role.Capabilities); int(rows) != expected {
log.Errorf("wrong number of role_capability rows created: %d expected: %d", rows, expected)
}
return nil, nil, http.StatusOK
}
func (role *TORole) deleteRoleCapabilityAssociations(tx *sqlx.Tx) (error, error, int) {
result, err := tx.Exec(deleteAssociatedCapabilities(), role.Name)
if err != nil {
return nil, errors.New("deleting role capabilities: " + err.Error()), http.StatusInternalServerError
}
if _, err = result.RowsAffected(); err != nil {
log.Errorf("could not check result after inserting role_capability relations: %v", err)
}
// TODO verify expected row count shouldn't be checked?
return nil, nil, http.StatusOK
}
func (role *TORole) Read(h http.Header, useIMS bool) ([]interface{}, error, error, int, *time.Time) {
api.DefaultSort(role.APIInfo(), "name")
vals, userErr, sysErr, errCode, maxTime := api.GenericRead(h, role, useIMS)
if errCode == http.StatusNotModified {
return []interface{}{}, nil, nil, errCode, maxTime
}
if userErr != nil || sysErr != nil {
return nil, userErr, sysErr, errCode, maxTime
}
returnable := []interface{}{}
for _, val := range vals {
rl := val.(*TORole)
caps := ([]string)(*rl.PQCapabilities)
rl.Capabilities = &caps
returnable = append(returnable, rl)
}
return returnable, nil, nil, http.StatusOK, maxTime
}
func (role *TORole) Update(h http.Header) (error, error, int) {
if ok, err := dbhelpers.RoleExists(role.ReqInfo.Tx.Tx, *role.ID); err != nil {
return nil, fmt.Errorf("verifying Role exists: %w", err), http.StatusInternalServerError
} else if !ok {
return errors.New("role not found"), nil, http.StatusNotFound
}
var isAdmin bool
if err := role.ReqInfo.Tx.Get(&isAdmin, isAdminQuery, role.ID); err != nil {
return nil, fmt.Errorf("checking if Role to be modified is '%s': %w", tc.AdminRoleName, err), http.StatusInternalServerError
}
if isAdmin {
return cannotModifyAdminError, nil, http.StatusBadRequest
}
if *role.PrivLevel > role.ReqInfo.User.PrivLevel {
return errors.New("can not create a role with a higher priv level than your own"), nil, http.StatusForbidden
}
if role.Capabilities != nil && *role.Capabilities != nil {
caps := *role.Capabilities
missing := role.ReqInfo.User.MissingPermissions(caps...)
if len(missing) != 0 {
return fmt.Errorf("cannot request more than assigned permissions, current user needs %s permissions", strings.Join(missing, ",")), nil, http.StatusForbidden
}
}
userErr, sysErr, errCode := api.GenericUpdate(h, role)
if userErr != nil || sysErr != nil {
return userErr, sysErr, errCode
}
// TODO cascade delete, to automatically do this in SQL?
if role.Capabilities != nil && *role.Capabilities != nil {
userErr, sysErr, errCode = role.deleteRoleCapabilityAssociations(role.ReqInfo.Tx)
if userErr != nil || sysErr != nil {
return userErr, sysErr, errCode
}
return role.createRoleCapabilityAssociations(role.ReqInfo.Tx)
}
return nil, nil, http.StatusOK
}
func (role *TORole) Delete() (error, error, int) {
if ok, err := dbhelpers.RoleExists(role.ReqInfo.Tx.Tx, *role.ID); err != nil {
return nil, fmt.Errorf("verifying Role exists: %w", err), http.StatusInternalServerError
} else if !ok {
return errors.New("role not found"), nil, http.StatusNotFound
}
assignedUsers := 0
if err := role.ReqInfo.Tx.Get(&assignedUsers, "SELECT COUNT(id) FROM public.tm_user WHERE role=$1", role.ID); err != nil {
return nil, errors.New("role delete counting assigned users: " + err.Error()), http.StatusInternalServerError
} else if assignedUsers != 0 {
return fmt.Errorf("can not delete a role with %d assigned users", assignedUsers), nil, http.StatusBadRequest
}
var isAdmin bool
if err := role.ReqInfo.Tx.Get(&isAdmin, isAdminQuery, role.ID); err != nil {
return nil, fmt.Errorf("checking if Role to be deleted is '%s': %w", tc.AdminRoleName, err), http.StatusInternalServerError
}
if isAdmin {
return cannotModifyAdminError, nil, http.StatusBadRequest
}
userErr, sysErr, errCode := api.GenericDelete(role)
if userErr != nil || sysErr != nil {
return userErr, sysErr, errCode
}
return role.deleteRoleCapabilityAssociations(role.ReqInfo.Tx)
}
func selectQuery() string {
return `SELECT
id,
name,
description,
priv_level,
ARRAY(SELECT rc.cap_name FROM role_capability AS rc WHERE rc.role_id=id) AS capabilities
FROM role`
}
func updateQuery() string {
return `UPDATE
role SET
name=:name,
description=:description
WHERE id=:id RETURNING last_updated`
}
func deleteAssociatedCapabilities() string {
return `DELETE FROM role_capability
WHERE role_id=(SELECT id from role r WHERE r.name=$1)`
}
func associateCapabilities() string {
return `INSERT INTO role_capability (
role_id,
cap_name) WITH
q1 AS ( SELECT * FROM (VALUES ($1::bigint)) AS role_id ),
q2 AS (SELECT UNNEST($2::text[]))
SELECT * FROM q1,q2`
}
func insertQuery() string {
return `INSERT INTO role (
name,
description,
priv_level
) VALUES (
:name,
:description,
:priv_level
)
RETURNING id, last_updated`
}
func deleteQuery() string {
return `DELETE FROM role WHERE id = :id`
}
// Update will modify the role identified by the role name.
func Update(w http.ResponseWriter, r *http.Request) {
var roleV4 tc.RoleV4
inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"name"}, nil)
if userErr != nil || sysErr != nil {
api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
return
}
defer inf.Close()
tx := inf.Tx.Tx
if err := json.NewDecoder(r.Body).Decode(&roleV4); err != nil {
api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
return
}
if err := roleV4.Validate(); err != nil {
api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
return
}
currentRoleName := inf.Params["name"]
if currentRoleName == tc.AdminRoleName {
api.HandleErr(w, r, tx, http.StatusBadRequest, cannotModifyAdminError, nil)
return
}
missing := inf.User.MissingPermissions(roleV4.Permissions...)
if len(missing) != 0 {
api.HandleErr(w, r, tx, http.StatusForbidden, fmt.Errorf("cannot request more than assigned permissions, current user needs %s permissions", strings.Join(missing, ",")), nil)
return
}
roleID, ok, err := dbhelpers.GetRoleIDFromName(tx, currentRoleName)
if err != nil {
api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
return
} else if !ok {
api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
return
}
existingLastUpdated, found, err := api.GetLastUpdatedByName(inf.Tx, currentRoleName, "role")
if err == nil && found == false {
api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
return
}
if err != nil {
api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
return
}
if !api.IsUnmodified(r.Header, *existingLastUpdated) {
api.HandleErr(w, r, tx, http.StatusPreconditionFailed, api.ResourceModifiedError, nil)
return
}
err = tx.QueryRow(updateRoleQuery(), roleV4.Name, roleV4.Description, currentRoleName).Scan(&roleV4.LastUpdated)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
return
}
usrErr, sysErr, code := api.ParseDBError(err)
api.HandleErr(w, r, tx, code, usrErr, fmt.Errorf("updating role and scanning lastUpdated : %w", sysErr))
return
}
userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleV4.Name)
if userErr != nil || sysErr != nil {
api.HandleErr(w, r, tx, errCode, userErr, sysErr)
return
}
userErr, sysErr, errCode = createRoleCapabilityAssociations(inf.Tx, roleID, &roleV4.Permissions)
if userErr != nil || sysErr != nil {
api.HandleErr(w, r, tx, errCode, userErr, sysErr)
return
}
alerts := tc.CreateAlerts(tc.SuccessLevel, "role was updated.")
// to return empty array instead of null
if roleV4.Permissions == nil {
roleV4.Permissions = []string{}
}
var roleResponse interface{}
roleResponse = tc.RoleV4{
Name: roleV4.Name,
Permissions: roleV4.Permissions,
Description: roleV4.Description,
LastUpdated: roleV4.LastUpdated,
}
api.WriteAlertsObj(w, r, http.StatusOK, alerts, roleResponse)
changeLogMsg := fmt.Sprintf("ROLE: %s, ID: %d, ACTION: Updated Role", roleV4.Name, roleID)
api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
}
func deleteRoleQuery() string {
return `DELETE FROM role WHERE name = $1`
}
func readQuery() string {
return `SELECT
name,
description,
last_updated,
ARRAY(SELECT rc.cap_name FROM role_capability AS rc WHERE rc.role_id=id) AS permissions
FROM role`
}
func createQuery() string {
return `INSERT INTO role (
name,
description,
priv_level
) VALUES (
$1,
$2,
$3
)
RETURNING id, last_updated`
}
func createRoleCapabilityAssociations(tx *sqlx.Tx, roleID int, permissions *[]string) (error, error, int) {
result, err := tx.Exec(associateCapabilities(), roleID, pq.Array(permissions))
if err != nil {
return nil, fmt.Errorf("creating role capabilities: %w", err), http.StatusInternalServerError
}
if rows, err := result.RowsAffected(); err != nil {
return nil, fmt.Errorf("could not check result after inserting role_capability relations: %w", err), http.StatusInternalServerError
} else if expected := int64(len(*permissions)); rows != expected {
return nil, fmt.Errorf("wrong number of role_capability rows created: %d expected: %d", rows, expected), http.StatusInternalServerError
}
return nil, nil, http.StatusOK
}
func deleteRoleCapabilityAssociations(tx *sqlx.Tx, roleName string) (error, error, int) {
_, err := tx.Exec(deleteAssociatedCapabilities(), roleName)
if err != nil {
return nil, fmt.Errorf("deleting role capabilities: %w", err), http.StatusInternalServerError
}
return nil, nil, http.StatusOK
}
// Delete will delete the role identified by the role name.
func Delete(w http.ResponseWriter, r *http.Request) {
inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"name"}, nil)
if userErr != nil || sysErr != nil {
api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
return
}
defer inf.Close()
tx := inf.Tx.Tx
roleName := inf.Params["name"]
if roleName == tc.AdminRoleName {
api.HandleErr(w, r, tx, http.StatusBadRequest, cannotModifyAdminError, nil)
return
}
assignedUsers := 0
if err := inf.Tx.Get(&assignedUsers, "SELECT COUNT(id) FROM tm_user WHERE role= (SELECT id FROM role r WHERE r.name=$1)", roleName); err != nil {
api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("role delete counting assigned users: %w", err))
return
} else if assignedUsers != 0 {
api.HandleErr(w, r, tx, http.StatusBadRequest, fmt.Errorf("can not delete a role with %d assigned users", assignedUsers), nil)
return
}
rows, err := tx.Query(deleteRoleQuery(), roleName)
if err != nil {
api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("deleting role: %w", err))
return
}
rows.Close()
alerts := tc.CreateAlerts(tc.SuccessLevel, "role was deleted.")
api.WriteAlerts(w, r, http.StatusOK, alerts)
changeLogMsg := fmt.Sprintf("ROLE: %s, ACTION: Deleted Role", roleName)
api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
}
// Create will create a new role based on the struct supplied.
func Create(w http.ResponseWriter, r *http.Request) {
var roleID int
var roleName string
var roleDesc string
var privLevel int
var roleCapabilities []string
var lastUpdated time.Time
var roleV4 tc.RoleV4
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
if err := json.NewDecoder(r.Body).Decode(&roleV4); err != nil {
api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
return
}
if err := roleV4.Validate(); err != nil {
api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
return
}
missing := inf.User.MissingPermissions(roleV4.Permissions...)
if len(missing) != 0 {
api.HandleErr(w, r, tx, http.StatusForbidden, fmt.Errorf("cannot request more than assigned permissions, current user needs %s permissions", strings.Join(missing, ",")), nil)
return
}
roleName = roleV4.Name
roleDesc = roleV4.Description
privLevel = inf.User.PrivLevel
roleCapabilities = roleV4.Permissions
rows, err := tx.Query(createQuery(), roleName, roleDesc, privLevel)
if err != nil {
usrErr, sysErr, code := api.ParseDBError(err)
api.HandleErr(w, r, tx, code, usrErr, fmt.Errorf("creating role: %w", sysErr))
return
}
defer rows.Close()
for rows.Next() {
if err := rows.Scan(&roleID, &lastUpdated); err != nil {
api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("role create: scanning role ID: %w", err))
return
}
}
if len(roleCapabilities) > 0 {
userErr, sysErr, errCode = createRoleCapabilityAssociations(inf.Tx, roleID, &roleCapabilities)
if userErr != nil || sysErr != nil {
api.HandleErr(w, r, tx, errCode, userErr, sysErr)
return
}
}
alerts := tc.CreateAlerts(tc.SuccessLevel, "role was created.")
// to return empty array instead of null
if roleCapabilities == nil {
roleCapabilities = []string{}
}
var roleResponse interface{}
capabilities := roleCapabilities
roleResponse = tc.RoleV4{
Name: roleName,
Permissions: capabilities,
Description: roleDesc,
LastUpdated: &lastUpdated,
}
api.WriteAlertsObj(w, r, http.StatusCreated, alerts, roleResponse)
changeLogMsg := fmt.Sprintf("ROLE: %s, ID: %d, ACTION: Created Role", roleName, roleID)
api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
}
// Get will read the roles and return them to the user.
func Get(w http.ResponseWriter, r *http.Request) {
var maxTime time.Time
var runSecond bool
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
params := make(map[string]dbhelpers.WhereColumnInfo, 1)
params["name"] = dbhelpers.WhereColumnInfo{Column: "name"}
if _, ok := inf.Params["orderby"]; !ok {
inf.Params["orderby"] = "name"
}
where, orderBy, pagination, queryValues, errs := dbhelpers.BuildWhereAndOrderByAndPagination(inf.Params, params)
if len(errs) != 0 {
api.HandleErr(w, r, tx, http.StatusBadRequest, util.JoinErrs(errs), nil)
return
}
if perm, ok := inf.Params["can"]; ok {
queryValues["can"] = perm
where = dbhelpers.AppendWhere(where, "permissions @> :can")
}
if inf.Config.UseIMS {
runSecond, maxTime = ims.TryIfModifiedSinceQuery(inf.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 := readQuery() + where + orderBy + pagination
rows, err := inf.Tx.NamedQuery(query, queryValues)
if err != nil {
api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("querying Roles: %w", err))
return
}
defer log.Close(rows, "reading in Roles from the database")
var roleV4 tc.RoleV4
rolesV4 := []tc.RoleV4{}
for rows.Next() {
if err = rows.Scan(&roleV4.Name, &roleV4.Description, &roleV4.LastUpdated, pq.Array(&roleV4.Permissions)); err != nil {
api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("scanning RoleV4 row: %w", err))
return
}
sort.Strings(roleV4.Permissions)
rolesV4 = append(rolesV4, roleV4)
}
api.WriteResp(w, r, rolesV4)
}
func selectMaxLastUpdatedQuery(where string) string {
return `SELECT max(t) FROM (
SELECT max(r.last_updated) AS t FROM role r ` + where + ` UNION ALL
SELECT max(l.last_updated) AS t FROM last_deleted l WHERE l.table_name='role' OR l.table_name='role_capability' UNION ALL
SELECT max(rc.last_updated) AS t FROM role_capability rc INNER JOIN role ON rc.role_id = role.id)
AS res`
}