blob: 87860fef9ddbd5074adb13c29f9201f4f4c54bc0 [file] [log] [blame]
package user
/*
* 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"
"strings"
"github.com/apache/trafficcontrol/lib/go-tc"
"github.com/apache/trafficcontrol/lib/go-tc/tovalidate"
"github.com/apache/trafficcontrol/lib/go-util"
validation "github.com/go-ozzo/ozzo-validation"
"github.com/go-ozzo/ozzo-validation/is"
"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"
)
const replacePasswordQuery = `
UPDATE tm_user
SET local_passwd=$1
WHERE id=$2
`
const replaceConfirmPasswordQuery = `
UPDATE tm_user
SET confirm_local_passwd=$1
WHERE id=$2
`
const replacePasswordV4Query = `
UPDATE tm_user
SET
confirm_local_passwd=$1,
local_passwd=$1
WHERE id=$2
`
const replaceCurrentQuery = `
UPDATE tm_user
SET
address_line1=$1,
address_line2=$2,
city=$3,
company=$4,
country=$5,
email=$6,
full_name=$7,
gid=$8,
new_user=FALSE,
phone_number=$9,
postal_code=$10,
public_ssh_key=$11,
role=$12,
state_or_province=$13,
tenant_id=$14,
token=NULL,
uid=$15,
username=$16
WHERE id=$17
RETURNING
address_line1,
address_line2,
city,
company,
country,
email,
full_name,
gid,
id,
last_updated,
new_user,
phone_number,
postal_code,
public_ssh_key,
role,
(
SELECT role.name
FROM role
WHERE role.id=tm_user.role
),
state_or_province,
(
SELECT tenant.name
FROM tenant
WHERE tenant.id=tm_user.tenant_id
),
tenant_id,
uid,
username
`
const replaceCurrentV4Query = `
UPDATE tm_user
SET
address_line1=$1,
address_line2=$2,
city=$3,
company=$4,
country=$5,
email=$6,
full_name=$7,
gid=$8,
new_user=FALSE,
phone_number=$9,
postal_code=$10,
public_ssh_key=$11,
role=(
SELECT role.id
FROM role
WHERE name=$12
),
state_or_province=$13,
tenant_id=$14,
token=NULL,
ucdn=$15,
uid=$16,
username=$17
WHERE id=$18
RETURNING
address_line1,
address_line2,
(
SELECT count(l.tm_user)
FROM log AS l
WHERE l.tm_user = tm_user.id
),
city,
company,
country,
email,
full_name,
gid,
id,
last_authenticated,
last_updated,
new_user,
phone_number,
postal_code,
public_ssh_key,
registration_sent,
(
SELECT role.name
FROM role
WHERE role.id=tm_user.role
),
state_or_province,
(
SELECT tenant.name
FROM tenant
WHERE tenant.id=tm_user.tenant_id
),
tenant_id,
ucdn,
uid,
username
`
func Current(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()
if inf.Version.Major < 4 {
cu, err := getLegacyUser(tx, inf.User.ID)
if err != nil {
api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting legacy current user: %w", err))
return
}
api.WriteResp(w, r, cu)
return
}
currentUser, err := getUser(inf.Tx.Tx, inf.User.ID)
if err != nil {
api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting current user: %w", err))
return
}
api.WriteResp(w, r, currentUser)
}
func getUser(tx *sql.Tx, id int) (tc.UserV4, error) {
q := `
SELECT
u.address_line1,
u.address_line2,
(
SELECT count(l.tm_user)
FROM log AS l
WHERE l.tm_user = u.id
),
u.city,
u.company,
u.country,
u.email,
u.full_name,
u.gid,
u.id,
u.last_authenticated,
u.last_updated,
u.new_user,
u.phone_number,
u.postal_code,
u.public_ssh_key,
u.registration_sent,
r.name as "role",
u.state_or_province,
t.name as tenant,
u.tenant_id,
u.ucdn,
u.uid,
u.username
FROM tm_user as u
LEFT JOIN role as r ON r.id = u.role
INNER JOIN tenant as t ON t.id = u.tenant_id
WHERE u.id=$1
`
var u tc.UserV4
err := tx.QueryRow(q, id).Scan(
&u.AddressLine1,
&u.AddressLine2,
&u.ChangeLogCount,
&u.City,
&u.Company,
&u.Country,
&u.Email,
&u.FullName,
&u.GID,
&u.ID,
&u.LastAuthenticated,
&u.LastUpdated,
&u.NewUser,
&u.PhoneNumber,
&u.PostalCode,
&u.PublicSSHKey,
&u.RegistrationSent,
&u.Role,
&u.StateOrProvince,
&u.Tenant,
&u.TenantID,
&u.UCDN,
&u.UID,
&u.Username,
)
if err != nil {
err = fmt.Errorf("querying current user: %w", err)
}
return u, err
}
func getLegacyUser(tx *sql.Tx, id int) (tc.UserCurrent, error) {
q := `
SELECT
u.address_line1,
u.address_line2,
u.city,
u.company,
u.country,
u.email,
u.full_name,
u.gid,
u.id,
u.last_updated,
u.local_passwd IS NOT NULL,
u.new_user,
u.phone_number,
u.postal_code,
u.public_ssh_key,
u.role as "role",
r.name as role_name,
u.state_or_province,
t.name as tenant,
u.tenant_id,
u.uid,
u.username
FROM tm_user as u
LEFT JOIN role as r ON r.id = u.role
INNER JOIN tenant as t ON t.id = u.tenant_id
WHERE u.id=$1
`
var u tc.UserCurrent
err := tx.QueryRow(q, id).Scan(
&u.AddressLine1,
&u.AddressLine2,
&u.City,
&u.Company,
&u.Country,
&u.Email,
&u.FullName,
&u.GID,
&u.ID,
&u.LastUpdated,
&u.LocalUser,
&u.NewUser,
&u.PhoneNumber,
&u.PostalCode,
&u.PublicSSHKey,
&u.Role,
&u.RoleName,
&u.StateOrProvince,
&u.Tenant,
&u.TenantID,
&u.UID,
&u.UserName,
)
if err != nil {
err = fmt.Errorf("querying legacy current user: %w", err)
}
return u, err
}
func ReplaceCurrent(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()
var userRequest tc.CurrentUserUpdateRequest
if err := json.NewDecoder(r.Body).Decode(&userRequest); err != nil {
errCode = http.StatusBadRequest
userErr = fmt.Errorf("couldn't parse request: %w", err)
api.HandleErr(w, r, tx, errCode, userErr, nil)
return
}
if userRequest.User == nil {
errCode = http.StatusBadRequest
userErr = fmt.Errorf("missing required 'user' object")
api.HandleErr(w, r, tx, errCode, userErr, nil)
return
}
user, exists, err := dbhelpers.GetUserByID(inf.User.ID, tx)
if err != nil {
sysErr = fmt.Errorf("getting user by ID %d: %w", inf.User.ID, err)
errCode = http.StatusInternalServerError
api.HandleErr(w, r, tx, errCode, nil, sysErr)
return
}
if !exists {
sysErr = fmt.Errorf("current user (#%d) doesn't exist... ??", inf.User.ID)
errCode = http.StatusInternalServerError
api.HandleErr(w, r, tx, errCode, nil, sysErr)
return
}
if err := userRequest.User.UnmarshalAndValidate(&user); err != nil {
errCode = http.StatusBadRequest
userErr = fmt.Errorf("couldn't parse request: %w", err)
api.HandleErr(w, r, tx, errCode, userErr, nil)
return
}
changePasswd := false
changeConfirmPasswd := false
// obfuscate passwords (UnmarshalAndValidate checks for equality with ConfirmLocalPassword)
// TODO: check for valid password via bad password list like Perl did? User creation doesn't...
if user.LocalPassword != nil && *user.LocalPassword != "" {
if ok, err := auth.IsGoodPassword(*user.LocalPassword); !ok {
errCode = http.StatusBadRequest
if err != nil {
userErr = err
} else {
userErr = errors.New("unacceptable password")
}
api.HandleErr(w, r, tx, errCode, userErr, nil)
return
}
hashPass, err := auth.DerivePassword(*user.LocalPassword)
if err != nil {
sysErr = fmt.Errorf("hashing new password: %w", err)
errCode = http.StatusInternalServerError
api.HandleErr(w, r, tx, errCode, nil, sysErr)
return
}
changePasswd = true
user.LocalPassword = util.StrPtr(hashPass)
}
// Perl did this although it serves no known purpose
if user.ConfirmLocalPassword != nil && *user.ConfirmLocalPassword != "" {
hashPass, err := auth.DerivePassword(*user.ConfirmLocalPassword)
if err != nil {
sysErr = fmt.Errorf("hashing new 'confirm' password: %w", err)
errCode = http.StatusInternalServerError
api.HandleErr(w, r, tx, errCode, nil, sysErr)
return
}
user.ConfirmLocalPassword = util.StrPtr(hashPass)
changeConfirmPasswd = true
}
if *user.Role != inf.User.Role {
privLevel, exists, err := dbhelpers.GetPrivLevelFromRoleID(tx, *user.Role)
if err != nil {
sysErr = fmt.Errorf("getting privLevel for Role #%d: %w", *user.Role, err)
errCode = http.StatusInternalServerError
api.HandleErr(w, r, tx, errCode, nil, sysErr)
return
}
if !exists {
userErr = fmt.Errorf("role: no such role: %d", *user.Role)
errCode = http.StatusNotFound
api.HandleErr(w, r, tx, errCode, userErr, nil)
return
}
if privLevel > inf.User.PrivLevel {
userErr = errors.New("role: cannot have greater permissions than user's current role")
errCode = http.StatusForbidden
api.HandleErr(w, r, tx, errCode, userErr, nil)
return
}
}
if ok, err := tenant.IsResourceAuthorizedToUserTx(*user.TenantID, inf.User, tx); err != nil {
if errors.Is(err, sql.ErrNoRows) {
userErr = errors.New("no such tenant")
errCode = http.StatusNotFound
} else {
sysErr = fmt.Errorf("checking user %s permissions on tenant #%d: %w", inf.User.UserName, *user.TenantID, err)
errCode = http.StatusInternalServerError
}
api.HandleErr(w, r, tx, errCode, userErr, sysErr)
return
} else if !ok {
// unlike Perl, this endpoint will not disclose the existence of tenants over which the current
// user has no permission - in keeping with the behavior of the '/tenants' endpoint.
userErr = errors.New("no such tenant")
errCode = http.StatusNotFound
api.HandleErr(w, r, tx, errCode, userErr, sysErr)
return
}
if *user.Username != inf.User.UserName {
if ok, err := dbhelpers.UsernameExists(*user.Username, tx); err != nil {
sysErr = fmt.Errorf("checking existence of user %s: %w", *user.Username, err)
errCode = http.StatusInternalServerError
api.HandleErr(w, r, tx, errCode, nil, sysErr)
return
} else if ok {
// TODO users are tenanted, so theoretically I should be hiding the existence of the
// conflicting user - but then how do I tell the client how to fix their request?
userErr = fmt.Errorf("username %s already exists", *user.Username)
errCode = http.StatusConflict
api.HandleErr(w, r, tx, errCode, userErr, nil)
return
}
}
if err = updateLegacyUser(&user, tx, changePasswd, changeConfirmPasswd); err != nil {
errCode = http.StatusInternalServerError
sysErr = fmt.Errorf("updating legacy user: %w", err)
api.HandleErr(w, r, tx, errCode, nil, sysErr)
return
}
api.WriteRespAlertObj(w, r, tc.SuccessLevel, "User profile was successfully updated", user)
}
func validateV4(user tc.UserV4, inf *api.APIInfo) (error, error) {
validateErrs := validation.Errors{
"email": validation.Validate(user.Email, validation.Required, is.Email),
"fullName": validation.Validate(user.FullName, validation.Required),
"role": validation.Validate(user.Role, validation.Required),
"username": validation.Validate(user.Username, validation.Required),
"tenantID": validation.Validate(user.TenantID, validation.Required),
}
// Password is not required for update
if user.LocalPassword != nil {
ok, err := auth.IsGoodLoginPair(user.Username, *user.LocalPassword)
if err != nil {
return err, nil
}
if !ok {
return errors.New("unacceptable password"), nil
}
}
if err := tovalidate.ToError(validateErrs); err != nil {
return err, nil
}
caps, err := dbhelpers.GetCapabilitiesFromRoleName(inf.Tx.Tx, user.Role)
if err != nil {
return nil, fmt.Errorf("getting capabilities for user's requested Role (%s): %w", user.Role, err)
}
missing := inf.User.MissingPermissions(caps...)
if len(missing) > 0 {
return nil, fmt.Errorf("cannot request more than assigned permissions, current user needs %s permissions", strings.Join(missing, ","))
}
if user.Username != inf.User.UserName {
if ok, err := dbhelpers.UsernameExists(user.Username, inf.Tx.Tx); err != nil {
return nil, fmt.Errorf("checking existence of user %s: %w", user.Username, err)
} else if ok {
return fmt.Errorf("username %s already exists", user.Username), nil
}
}
return nil, nil
}
// ReplaceCurrentV4 replaces the current user with the definition in the user's
// request (assuming it meets validation constraints).
func ReplaceCurrentV4(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()
var user tc.UserV4
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
errCode = http.StatusBadRequest
userErr = fmt.Errorf("couldn't parse request: %w", err)
api.HandleErr(w, r, tx, errCode, userErr, nil)
return
}
// Token must never be updated this way
user.Token = nil
user.ID = new(int)
*user.ID = inf.User.ID
userErr, sysErr = validateV4(user, inf)
if userErr != nil || sysErr != nil {
errCode = http.StatusBadRequest
if sysErr != nil {
errCode = http.StatusInternalServerError
}
api.HandleErr(w, r, tx, errCode, userErr, sysErr)
return
}
changePasswd := false
// obfuscate password
if user.LocalPassword != nil {
hashPass, err := auth.DerivePassword(*user.LocalPassword)
if err != nil {
sysErr = fmt.Errorf("hashing new password for user %s (#%d): %w", inf.User.UserName, inf.User.ID, err)
errCode = http.StatusInternalServerError
api.HandleErr(w, r, tx, errCode, nil, sysErr)
return
}
changePasswd = true
*user.LocalPassword = hashPass
}
if ok, err := tenant.IsResourceAuthorizedToUserTx(user.TenantID, inf.User, tx); err != nil {
if errors.Is(err, sql.ErrNoRows) {
userErr = fmt.Errorf("no such tenant: #%d", user.TenantID)
errCode = http.StatusNotFound
} else {
sysErr = fmt.Errorf("checking user %s permissions on tenant #%d: %w", inf.User.UserName, user.TenantID, err)
errCode = http.StatusInternalServerError
}
api.HandleErr(w, r, tx, errCode, userErr, sysErr)
return
} else if !ok {
userErr = fmt.Errorf("no such tenant: #%d", user.TenantID)
errCode = http.StatusNotFound
api.HandleErr(w, r, tx, errCode, userErr, sysErr)
return
}
if err := updateUser(&user, tx, changePasswd); err != nil {
errCode = http.StatusInternalServerError
sysErr = fmt.Errorf("updating user: %w", err)
api.HandleErr(w, r, tx, errCode, nil, sysErr)
return
}
api.WriteRespAlertObj(w, r, tc.SuccessLevel, "User profile was successfully updated", user)
}
func updateLegacyUser(u *tc.User, tx *sql.Tx, changePassword bool, changeConfirmPasswd bool) error {
row := tx.QueryRow(replaceCurrentQuery,
u.AddressLine1,
u.AddressLine2,
u.City,
u.Company,
u.Country,
u.Email,
u.FullName,
u.GID,
u.PhoneNumber,
u.PostalCode,
u.PublicSSHKey,
u.Role,
u.StateOrProvince,
u.TenantID,
u.UID,
u.Username,
u.ID,
)
err := row.Scan(
&u.AddressLine1,
&u.AddressLine2,
&u.City,
&u.Company,
&u.Country,
&u.Email,
&u.FullName,
&u.GID,
&u.ID,
&u.LastUpdated,
&u.NewUser,
&u.PhoneNumber,
&u.PostalCode,
&u.PublicSSHKey,
&u.Role,
&u.RoleName,
&u.StateOrProvince,
&u.Tenant,
&u.TenantID,
&u.UID,
&u.Username,
)
if err != nil {
return err
}
if changePassword {
_, err = tx.Exec(replacePasswordQuery, u.LocalPassword, u.ID)
if err != nil {
return fmt.Errorf("resetting password: %w", err)
}
}
if changeConfirmPasswd {
_, err = tx.Exec(replaceConfirmPasswordQuery, u.ConfirmLocalPassword, u.ID)
if err != nil {
return fmt.Errorf("resetting confirm password: %w", err)
}
}
u.LocalPassword = nil
u.ConfirmLocalPassword = nil
return nil
}
func updateUser(u *tc.UserV4, tx *sql.Tx, changePassword bool) error {
row := tx.QueryRow(replaceCurrentV4Query,
u.AddressLine1,
u.AddressLine2,
u.City,
u.Company,
u.Country,
u.Email,
u.FullName,
u.GID,
u.PhoneNumber,
u.PostalCode,
u.PublicSSHKey,
u.Role,
u.StateOrProvince,
u.TenantID,
u.UCDN,
u.UID,
u.Username,
u.ID,
)
err := row.Scan(
&u.AddressLine1,
&u.AddressLine2,
&u.ChangeLogCount,
&u.City,
&u.Company,
&u.Country,
&u.Email,
&u.FullName,
&u.GID,
&u.ID,
&u.LastAuthenticated,
&u.LastUpdated,
&u.NewUser,
&u.PhoneNumber,
&u.PostalCode,
&u.PublicSSHKey,
&u.RegistrationSent,
&u.Role,
&u.StateOrProvince,
&u.Tenant,
&u.TenantID,
&u.UCDN,
&u.UID,
&u.Username,
)
if err != nil {
return err
}
if changePassword {
_, err = tx.Exec(replacePasswordQuery, u.LocalPassword, u.ID)
if err != nil {
return fmt.Errorf("resetting password: %w", err)
}
}
u.LocalPassword = nil
return nil
}