| 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 |
| } |