You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by GitBox <gi...@apache.org> on 2019/01/15 22:11:18 UTC

[trafficcontrol] Diff for: [GitHub] mitchell852 closed pull request #2338: Add TO Go users

diff --git a/traffic_ops/traffic_ops_golang/api/api.go b/traffic_ops/traffic_ops_golang/api/api.go
index d67d91436..1c43cbd6c 100644
--- a/traffic_ops/traffic_ops_golang/api/api.go
+++ b/traffic_ops/traffic_ops_golang/api/api.go
@@ -158,6 +158,34 @@ func WriteRespAlertObj(w http.ResponseWriter, r *http.Request, level tc.AlertLev
 	w.Write(respBts)
 }
 
+// WriteRespAlert is like WriteResp, but also takes an alert level and message to write at the root level.
+// This is a helper for the common case; not using this in unusual cases is perfectly acceptable.
+func WriteRespWithAlert(w http.ResponseWriter, r *http.Request, v interface{}, level tc.AlertLevel, msg string) {
+	vals := map[string]interface{}{
+		"alerts":   tc.CreateAlerts(level, msg).Alerts,
+		"response": v,
+	}
+	respBts, err := json.Marshal(vals)
+	if err != nil {
+		HandleErr(w, r, http.StatusInternalServerError, nil, fmt.Errorf("marshalling JSON for %T: %v", v, err))
+		return
+	}
+	w.Header().Set(tc.ContentType, tc.ApplicationJson)
+	w.Write(respBts)
+}
+
+// RespAlertWriter is like RespWriter, but also takes an alert level and message to write at the root level
+// This is a helper for the common case; not using this in unusual cases is perfectly acceptable.
+func RespWithAlertWriter(w http.ResponseWriter, r *http.Request, level tc.AlertLevel, msg string) func(v interface{}, err error) {
+	return func(v interface{}, err error) {
+		if err != nil {
+			HandleErr(w, r, http.StatusInternalServerError, nil, err)
+			return
+		}
+		WriteRespWithAlert(w, r, v, level, msg)
+	}
+}
+
 // IntParams parses integer parameters, and returns map of the given params, or an error if any integer param is not an integer. The intParams may be nil if no integer parameters are required. Note this does not check existence; if an integer paramter is required, it should be included in the requiredParams given to NewInfo.
 // This is a helper for the common case; not using this in unusual cases is perfectly acceptable.
 func IntParams(params map[string]string, intParamNames []string) (map[string]int, error) {
diff --git a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
index a20f1dd62..d67093d77 100644
--- a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
+++ b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
@@ -22,6 +22,8 @@ package dbhelpers
 import (
 	"database/sql"
 	"errors"
+	"reflect"
+	"strconv"
 	"strings"
 
 	"github.com/apache/trafficcontrol/lib/go-log"
@@ -101,6 +103,53 @@ func parseCriteriaAndQueryValues(queryParamsToSQLCols map[string]WhereColumnInfo
 	return criteria, queryValues, errs
 }
 
+// BuildInsertColumns creates the insert query columns, the query parameters, and the slice of values to insert, from a struct.
+// Nil pointers are not included. This allows dynamically inserting into columns which are `not null default`, where a naïve query would fail with not null conflicts.
+// This does not return the entire query as a string, so where clauses, returning statements, and other modifications may be used.
+// The struct must have `db:"column_name"` tags for the column names to insert.
+//
+// Example:
+//  cols, params, vals := buildInsertColumns(u)
+//  db.Exec(`INSERT INTO my_table (` + cols + `) VALUES (` + params + `)`, vals..)
+// ```
+//
+func BuildInsertColumns(v interface{}) (string, string, []interface{}, error) {
+	cols := []string{}
+	vals := []interface{}{}
+	params := []string{}
+	colI := 1 // SQL query parameters start at 1
+	val := reflect.Indirect(reflect.ValueOf(v))
+	if val.Type().Kind() != reflect.Struct {
+		return "", "", nil, errors.New("value must be a struct or a pointer to a struct")
+	}
+	// appendVals is a recursing function, which calls itself on anonymous embedded structs.
+	// This way, embedded struct members are treated like direct members, and included in the columns.
+	appendVals := (func(reflect.Value))(nil)
+	appendVals = func(val reflect.Value) {
+		for i := 0; i < val.NumField(); i++ {
+			field := val.Field(i)
+			typeField := val.Type().Field(i)
+			if field.Kind() == reflect.Struct && typeField.Anonymous {
+				appendVals(field)
+				continue
+			}
+			dbTag := typeField.Tag.Get("db")
+			if dbTag == "" {
+				continue
+			}
+			if field.Kind() == reflect.Ptr && field.IsNil() {
+				continue
+			}
+			cols = append(cols, dbTag)
+			vals = append(vals, field.Interface())
+			params = append(params, "$"+strconv.Itoa(colI))
+			colI++
+		}
+	}
+	appendVals(val)
+	return strings.Join(cols, ","), strings.Join(params, ","), vals, nil
+}
+
 //parses pq errors for uniqueness constraint violations
 func ParsePQUniqueConstraintError(err *pq.Error) (error, tc.ApiErrorType) {
 	if len(err.Constraint) > 0 && len(err.Detail) > 0 { //we only want to continue parsing if it is a constraint error with details
diff --git a/traffic_ops/traffic_ops_golang/routes.go b/traffic_ops/traffic_ops_golang/routes.go
index c33e66628..fd015df3b 100644
--- a/traffic_ops/traffic_ops_golang/routes.go
+++ b/traffic_ops/traffic_ops_golang/routes.go
@@ -148,6 +148,11 @@ func Routes(d ServerData) ([]Route, []RawRoute, http.Handler, error) {
 
 		{1.1, http.MethodGet, `user/current/?(\.json)?$`, user.Current, auth.PrivLevelReadOnly, Authenticated, nil},
 
+		{1.1, http.MethodGet, `users/?(\.json)?$`, user.Get(d.DB.DB), auth.PrivLevelReadOnly, Authenticated, nil},
+		{1.1, http.MethodGet, `users/{id}?(\.json)?$`, user.GetID(d.DB.DB), auth.PrivLevelReadOnly, Authenticated, nil},
+		{1.1, http.MethodPut, `users/{id}?(\.json)?$`, user.Put(d.DB), auth.PrivLevelOperations, Authenticated, nil},
+		{1.1, http.MethodPost, `users/?(\.json)?$`, user.Post(d.DB.DB), auth.PrivLevelOperations, Authenticated, nil},
+
 		//Parameter: CRUD
 		{1.1, http.MethodGet, `parameters/?(\.json)?$`, api.ReadHandler(parameter.GetTypeSingleton()), auth.PrivLevelReadOnly, Authenticated, nil},
 		{1.1, http.MethodGet, `parameters/{id}$`, api.ReadHandler(parameter.GetTypeSingleton()), auth.PrivLevelReadOnly, Authenticated, nil},
diff --git a/traffic_ops/traffic_ops_golang/user/user.go b/traffic_ops/traffic_ops_golang/user/user.go
new file mode 100644
index 000000000..871864c59
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/user/user.go
@@ -0,0 +1,406 @@
+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"
+	"net/http"
+	"strconv"
+	"strings"
+
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"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"
+
+	"github.com/jmoiron/sqlx"
+)
+
+func Get(db *sql.DB) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		_, intParams, userErr, sysErr, errCode := api.AllParams(r, nil, []string{"tenant"})
+		if userErr != nil || sysErr != nil {
+			api.HandleErr(w, r, errCode, userErr, sysErr)
+			return
+		}
+		if tenantID, ok := intParams["tenant"]; ok {
+			api.RespWriter(w, r)(getUsersByTenantID(db, tenantID))
+			return
+		}
+		api.RespWriter(w, r)(getUsers(db))
+	}
+}
+
+func GetID(db *sql.DB) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		_, intParams, userErr, sysErr, errCode := api.AllParams(r, []string{"id"}, []string{"id"})
+		if userErr != nil || sysErr != nil {
+			api.HandleErr(w, r, errCode, userErr, sysErr)
+			return
+		}
+		user, ok, err := getUserByID(db, intParams["id"])
+		if err != nil {
+			api.HandleErr(w, r, http.StatusInternalServerError, nil, errors.New("getting user "+strconv.Itoa(intParams["id"])+": "+err.Error()))
+			return
+		}
+		if !ok {
+			api.HandleErr(w, r, http.StatusNotFound, nil, nil)
+			return
+		}
+		api.WriteResp(w, r, []tc.APIUser{user})
+	}
+}
+
+func Post(db *sql.DB) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		u := tc.APIUserPost{}
+		if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
+			api.HandleErr(w, r, http.StatusBadRequest, errors.New("malformed JSON"), nil)
+			return
+		}
+		if err := validatePost(u); err != nil {
+			api.HandleErr(w, r, http.StatusBadRequest, errors.New("validation error: "+err.Error()), nil)
+			return
+		}
+		api.RespWithAlertWriter(w, r, tc.SuccessLevel, "User creation was successful.")(createUser(db, u))
+	}
+}
+
+func Put(db *sqlx.DB) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		user, err := auth.GetCurrentUser(r.Context())
+		if err != nil {
+			api.HandleErr(w, r, http.StatusInternalServerError, nil, errors.New("getting current user: "+err.Error()))
+			return
+		}
+		_, intParams, userErr, sysErr, errCode := api.AllParams(r, []string{"id"}, []string{"id"})
+		if userErr != nil || sysErr != nil {
+			api.HandleErr(w, r, errCode, userErr, sysErr)
+			return
+		}
+		existingTenantID, ok, err := getUserTenantIDByID(db.DB, intParams["id"])
+		if err != nil {
+			api.HandleErr(w, r, http.StatusInternalServerError, nil, errors.New("getting user "+strconv.Itoa(intParams["id"])+": "+err.Error()))
+			return
+		}
+		if !ok {
+			api.HandleErr(w, r, http.StatusNotFound, nil, nil)
+			return
+		}
+		u := tc.APIUserPost{}
+		if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
+			log.Errorln("user put: malformed JSON: " + err.Error())
+			api.HandleErr(w, r, http.StatusBadRequest, errors.New("malformed JSON"), nil)
+			return
+		}
+		userID := intParams["id"]
+		u.ID = &userID
+		if err := validatePut(u); err != nil {
+			api.HandleErr(w, r, http.StatusBadRequest, errors.New("validation error: "+err.Error()), nil)
+			return
+		}
+		authorized, err := isTenantAuthorized(user, db, intParams["id"], existingTenantID, u.TenantID)
+		if err != nil {
+			api.HandleErr(w, r, http.StatusInternalServerError, nil, errors.New("checking user tenancy for "+strconv.Itoa(intParams["id"])+": "+err.Error()))
+			return
+		}
+		if !authorized {
+			log.Errorln("user put: tenant unauthorized!")
+			api.HandleErr(w, r, http.StatusUnauthorized, nil, nil)
+			return
+		}
+		api.RespWithAlertWriter(w, r, tc.SuccessLevel, "User update successful.")(updateUser(db.DB, u))
+	}
+}
+
+// isTenantAuthorized returns whether the current user is authorized to modify the tenant of the given user, and any new tenant. The tenantID may be null, if the existing tenant is not being changed.
+func isTenantAuthorized(user *auth.CurrentUser, db *sqlx.DB, userID int, oldTenantID *int, newTenantID *int) (bool, error) {
+	if oldTenantID != nil {
+		authorized, err := tenant.IsResourceAuthorizedToUser(*oldTenantID, user, db)
+		if err != nil {
+			return false, errors.New("checking authorization for existing user ID: " + err.Error())
+		}
+		if !authorized {
+			return false, nil
+		}
+	}
+	if newTenantID != nil && (oldTenantID == nil || *newTenantID != *oldTenantID) {
+		authorized, err := tenant.IsResourceAuthorizedToUser(*newTenantID, user, db)
+		if err != nil {
+			return false, errors.New("checking authorization for new user ID: " + err.Error())
+		}
+		if !authorized {
+			return false, nil
+		}
+	}
+	return true, nil
+}
+
+func validatePost(u tc.APIUserPost) error {
+	errs := []string{}
+	if u.ConfirmLocalPassword == nil || *u.ConfirmLocalPassword == "" {
+		errs = append(errs, "confirmLocalPassword must be set")
+	}
+	if u.Email == nil || *u.Email == "" {
+		errs = append(errs, "email must be set")
+	}
+	if u.FullName == nil || *u.FullName == "" {
+		errs = append(errs, "fullName must be set")
+	}
+	if u.LocalPassword == nil || *u.LocalPassword == "" {
+		errs = append(errs, "localPassword must be set")
+	}
+	if u.Role == nil {
+		errs = append(errs, "role must be set")
+	}
+	if u.UserName == nil || *u.UserName == "" {
+		errs = append(errs, "username must be set")
+	}
+	if len(errs) > 0 {
+		return errors.New(strings.Join(errs, "; "))
+	}
+	return nil
+}
+
+func validatePut(u tc.APIUserPost) error {
+	errs := []string{}
+	if u.Email == nil || *u.Email == "" {
+		errs = append(errs, "email must be set")
+	}
+	if u.FullName == nil || *u.FullName == "" {
+		errs = append(errs, "fullName must be set")
+	}
+	if u.Role == nil {
+		errs = append(errs, "role must be set")
+	}
+	if u.UserName == nil || *u.UserName == "" {
+		errs = append(errs, "username must be set")
+	}
+	if len(errs) > 0 {
+		return errors.New(strings.Join(errs, "; "))
+	}
+	return nil
+}
+
+func getUsers(db *sql.DB) ([]tc.APIUser, 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.new_user,
+u.phone_number,
+u.postal_code,
+u.public_ssh_key,
+u.registration_sent,
+u.role,
+r.name,
+u.state_or_province,
+t.name,
+u.tenant_id,
+u.uid,
+u.username
+FROM tm_user as u
+JOIN tenant as t on t.id = u.tenant_id
+JOIN role as r on r.id = u.role
+`
+	rows, err := db.Query(q)
+	if err != nil {
+		return nil, errors.New("querying users: " + err.Error())
+	}
+	defer rows.Close()
+	users := []tc.APIUser{}
+	for rows.Next() {
+		u := tc.APIUser{}
+		if err := rows.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.RegistrationSent, &u.Role, &u.RoleName, &u.StateOrProvince, &u.Tenant, &u.TenantID, &u.UID, &u.UserName); err != nil {
+			return nil, errors.New("scanning users: " + err.Error())
+		}
+		users = append(users, u)
+	}
+	return users, nil
+}
+
+func getUsersByTenantID(db *sql.DB, tenantID int) ([]tc.APIUser, 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.new_user,
+u.phone_number,
+u.postal_code,
+u.public_ssh_key,
+u.registration_sent,
+u.role,
+r.name,
+u.state_or_province,
+t.name,
+u.tenant_id,
+u.uid,
+u.username
+FROM tm_user as u
+JOIN tenant as t on t.id = u.tenant_id
+JOIN role as r on r.id = u.role
+WHERE u.tenant_id = $1
+`
+	rows, err := db.Query(q, tenantID)
+	if err != nil {
+		return nil, errors.New("querying users: " + err.Error())
+	}
+	defer rows.Close()
+	users := []tc.APIUser{}
+	for rows.Next() {
+		u := tc.APIUser{}
+		if err := rows.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.RegistrationSent, &u.Role, &u.RoleName, &u.StateOrProvince, &u.Tenant, &u.TenantID, &u.UID, &u.UserName); err != nil {
+			return nil, errors.New("scanning users: " + err.Error())
+		}
+		users = append(users, u)
+	}
+	return users, nil
+}
+
+func getUserByID(db *sql.DB, id int) (tc.APIUser, bool, 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.new_user,
+u.phone_number,
+u.postal_code,
+u.public_ssh_key,
+u.registration_sent,
+u.role,
+r.name,
+u.state_or_province,
+t.name,
+u.tenant_id,
+u.uid,
+u.username
+FROM tm_user as u
+JOIN tenant as t on t.id = u.tenant_id
+JOIN role as r on r.id = u.role
+WHERE u.id = $1
+`
+	u := tc.APIUser{}
+	if err := db.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.NewUser, &u.PhoneNumber, &u.PostalCode, &u.PublicSSHKey, &u.RegistrationSent, &u.Role, &u.RoleName, &u.StateOrProvince, &u.Tenant, &u.TenantID, &u.UID, &u.UserName); err != nil {
+		if err == sql.ErrNoRows {
+			return tc.APIUser{}, false, nil
+		}
+		return tc.APIUser{}, true, errors.New("querying user: " + err.Error())
+	}
+	return u, true, nil
+}
+
+func createUser(db *sql.DB, u tc.APIUserPost) (tc.APIUser, error) {
+	cols, params, vals, err := dbhelpers.BuildInsertColumns(u)
+	if err != nil {
+		return tc.APIUser{}, errors.New("building insert query: " + err.Error())
+	}
+	q := `INSERT INTO tm_user (` + cols + `) VALUES (` + params + `) RETURNING id, last_updated, (select r2.name from role as r2 where id = tm_user.role)`
+	if err := db.QueryRow(q, vals...).Scan(&u.ID, &u.LastUpdated, &u.RoleName); err != nil {
+		return tc.APIUser{}, errors.New("inserting user: " + err.Error())
+	}
+	return u.APIUser, nil
+}
+
+func updateUser(db *sql.DB, u tc.APIUserPost) (tc.APIUser, error) {
+	q := `
+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=$9,
+phone_number=$10,
+postal_code=$11,
+public_ssh_key=$12,
+registration_sent=$13,
+role=$14,
+state_or_province=$15,
+tenant_id=$16,
+uid=$17,
+username=$18
+`
+	nextParam := `$19`
+	if u.LocalPassword != nil {
+		q += `,local_passwd=$19`
+		nextParam = `$20`
+	}
+	if u.ConfirmLocalPassword != nil {
+		if u.LocalPassword == nil {
+			q += `,confirm_local_passwd=$19`
+			nextParam = `$20`
+		} else {
+			q += `,confirm_local_passwd=$20`
+			nextParam = `$21`
+		}
+	}
+
+	q += `
+WHERE id=` + nextParam + `
+RETURNING
+last_updated,
+(select t2.name from tenant as t2 where id = tm_user.tenant_id),
+(select r2.name from role as r2 where id = tm_user.role)
+`
+	vals := []interface{}{u.AddressLine1, u.AddressLine2, u.City, u.Company, u.Country, u.Email, u.FullName, u.GID, u.NewUser, u.PhoneNumber, u.PostalCode, u.PublicSSHKey, u.RegistrationSent, u.Role, u.StateOrProvince, u.TenantID, u.UID, u.UserName}
+	if u.LocalPassword != nil {
+		vals = append(vals, u.LocalPassword)
+	}
+	if u.ConfirmLocalPassword != nil {
+		vals = append(vals, u.ConfirmLocalPassword)
+	}
+	vals = append(vals, u.ID)
+
+	if err := db.QueryRow(q, vals...).Scan(&u.LastUpdated, &u.Tenant, &u.RoleName); err != nil {
+		return tc.APIUser{}, errors.New("updating user: " + err.Error())
+	}
+	return u.APIUser, nil
+}


With regards,
Apache Git Services