blob: d1913d94ce63b5f69edac4894fde7dcd62450618 [file] [log] [blame]
package auth
/*
* 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 (
"context"
"crypto/sha1"
"database/sql"
"encoding/hex"
"errors"
"fmt"
"net/http"
"time"
"github.com/apache/trafficcontrol/lib/go-tc"
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
"github.com/jmoiron/sqlx"
"github.com/lib/pq"
)
type CurrentUser struct {
UserName string `json:"userName" db:"username"`
ID int `json:"id" db:"id"`
PrivLevel int `json:"privLevel" db:"priv_level"`
TenantID int `json:"tenantId" db:"tenant_id"`
Role int `json:"role" db:"role"`
RoleName string `json:"roleName" db:"role_name"`
Capabilities pq.StringArray `json:"capabilities" db:"capabilities"`
UCDN string `json:"ucdn" db:"ucdn"`
perms map[string]struct{}
}
// Can returns whether or not the user has the specified Permission, i.e.
// whether or not they "can" do something.
func (cu CurrentUser) Can(permission string) bool {
if cu.RoleName == tc.AdminRoleName {
return true
}
_, ok := cu.perms[permission]
return ok
}
// MissingPermissions returns all of the passed Permissions that the user does
// not have.
func (cu CurrentUser) MissingPermissions(permissions ...string) []string {
var ret []string
if cu.RoleName == tc.AdminRoleName {
return ret
}
for _, perm := range permissions {
if _, ok := cu.perms[perm]; !ok {
ret = append(ret, perm)
}
}
return ret
}
type PasswordForm struct {
Username string `json:"u"`
Password string `json:"p"`
}
const disallowed = "disallowed"
// PrivLevelInvalid - The Default Priv level
const PrivLevelInvalid = -1
const PrivLevelUnauthenticated = 0
const PrivLevelReadOnly = 10
const PrivLevelSteering = 15
const PrivLevelFederation = 15
const PrivLevelPortal = 15
const PrivLevelOperations = 20
const PrivLevelAdmin = 30
// TenantIDInvalid - The default Tenant ID
const TenantIDInvalid = -1
type key int
const CurrentUserKey key = iota
// GetCurrentUserFromDB - returns the id and privilege level of the given user along with the username, or -1 as the id, - as the userName and PrivLevelInvalid if the user doesn't exist, along with a user facing error, a system error to log, and an error code to return
func GetCurrentUserFromDB(DB *sqlx.DB, user string, timeout time.Duration) (CurrentUser, error, error, int) {
invalidUser := CurrentUser{"-", -1, PrivLevelInvalid, TenantIDInvalid, -1, "", []string{}, "", nil}
if usersCacheIsEnabled() {
u, exists := getUserFromCache(user)
if !exists {
return invalidUser, errors.New("user not found"), fmt.Errorf("checking user '%s' info: user not in cache", user), http.StatusUnauthorized
}
return u.CurrentUser, nil, nil, http.StatusOK
}
qry := `
SELECT
r.priv_level,
r.id as role,
r.name as role_name,
u.id,
u.username,
u.tenant_id,
ARRAY(SELECT rc.cap_name FROM role_capability AS rc WHERE rc.role_id=r.id) AS capabilities,
u.ucdn
FROM
tm_user AS u
JOIN
role AS r ON u.role = r.id
WHERE
u.username = $1
`
var currentUserInfo CurrentUser
if DB == nil {
return CurrentUser{"-", -1, PrivLevelInvalid, TenantIDInvalid, -1, "", []string{}, "", nil}, nil, errors.New("no db provided to GetCurrentUserFromDB"), http.StatusInternalServerError
}
dbCtx, dbClose := context.WithTimeout(context.Background(), timeout)
defer dbClose()
err := DB.GetContext(dbCtx, &currentUserInfo, qry, user)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return invalidUser, errors.New("user not found"), fmt.Errorf("checking user %v info: user not in database", user), http.StatusUnauthorized
}
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
return invalidUser, nil, fmt.Errorf("db access timed out: %w number of open connections: %d", err, DB.Stats().OpenConnections), http.StatusServiceUnavailable
}
return invalidUser, nil, fmt.Errorf("checking user %v info: %w", user, err), http.StatusInternalServerError
}
currentUserInfo.perms = make(map[string]struct{}, len(currentUserInfo.Capabilities))
for _, perm := range currentUserInfo.Capabilities {
currentUserInfo.perms[perm] = struct{}{}
}
return currentUserInfo, nil, nil, http.StatusOK
}
func GetCurrentUser(ctx context.Context) (*CurrentUser, error) {
val := ctx.Value(CurrentUserKey)
if val != nil {
switch v := val.(type) {
case CurrentUser:
return &v, nil
default:
return nil, fmt.Errorf("CurrentUser found with bad type: %T", v)
}
}
return &CurrentUser{"-", -1, PrivLevelInvalid, TenantIDInvalid, -1, "", []string{}, "", nil}, errors.New("No user found in Context")
}
func CheckLocalUserIsAllowed(form PasswordForm, db *sqlx.DB, ctx context.Context) (bool, error, error) {
if usersCacheIsEnabled() {
u, exists := getUserFromCache(form.Username)
if !exists {
return false, fmt.Errorf("user '%s' not found in cache", form.Username), nil
}
allowed := u.RoleName != disallowed
return allowed, nil, nil
}
var roleName string
err := db.GetContext(ctx, &roleName, "SELECT role.name FROM role INNER JOIN tm_user ON tm_user.role = role.id where username=$1", form.Username)
if err != nil {
if err == context.DeadlineExceeded || err == context.Canceled {
return false, nil, err
}
return false, err, nil
}
if roleName != "" {
if roleName != disallowed { //relies on unchanging role name assumption.
return true, nil, nil
}
}
return false, nil, nil
}
// GetUserUcdn returns the Upstream CDN to which the user belongs for CDNi operations.
func GetUserUcdn(form PasswordForm, db *sqlx.DB, ctx context.Context) (string, error) {
if usersCacheIsEnabled() {
u, exists := getUserFromCache(form.Username)
if !exists {
return "", fmt.Errorf("user '%s' not found in cache", form.Username)
}
return u.UCDN, nil
}
var ucdn string
err := db.GetContext(ctx, &ucdn, "SELECT ucdn FROM tm_user where username=$1", form.Username)
if err != nil {
return "", err
}
return ucdn, nil
}
func CheckLocalUserPassword(form PasswordForm, db *sqlx.DB, ctx context.Context) (bool, error, error) {
var hashedPassword string
if usersCacheIsEnabled() {
u, exists := getUserFromCache(form.Username)
if !exists {
return false, fmt.Errorf("user '%s' not found in cache", form.Username), nil
}
if u.LocalPasswd == nil {
return false, nil, nil
}
hashedPassword = *u.LocalPasswd
} else {
err := db.GetContext(ctx, &hashedPassword, "SELECT local_passwd FROM tm_user WHERE username=$1", form.Username)
if err != nil {
if err == context.DeadlineExceeded || err == context.Canceled {
return false, nil, err
}
return false, err, nil
}
}
err := VerifySCRYPTPassword(form.Password, hashedPassword)
if err != nil {
hashedInput, err := sha1Hex(form.Password)
if err != nil {
return false, err, nil
}
if hashedPassword == hashedInput { // for backwards compatibility
return true, nil, nil
}
return false, err, nil
}
return true, nil, nil
}
// CheckLocalUserToken checks the passed token against the records in the db for a match, up to a
// maximum duration of timeout.
func CheckLocalUserToken(token string, db *sqlx.DB, timeout time.Duration) (bool, string, error) {
if usersCacheIsEnabled() {
username, matched := getUserNameFromCacheByToken(token)
return matched, username, nil
}
dbCtx, dbClose := context.WithTimeout(context.Background(), timeout)
defer dbClose()
var username string
err := db.GetContext(dbCtx, &username, `SELECT username FROM tm_user WHERE token=$1 AND role!=(SELECT role.id FROM role WHERE role.name=$2)`, token, disallowed)
if err != nil {
if err == sql.ErrNoRows {
return false, "", nil
}
return false, "", err
}
return true, username, nil
}
func sha1Hex(s string) (string, error) {
// SHA1 hash
hash := sha1.New()
if _, err := hash.Write([]byte(s)); err != nil {
return "", err
}
hashBytes := hash.Sum(nil)
hexSha1 := hex.EncodeToString(hashBytes)
return hexSha1, nil
}
func CheckLDAPUser(form PasswordForm, cfg *config.ConfigLDAP) (bool, error) {
userDN, valid, err := LookupUserDN(form.Username, cfg)
if err != nil {
return false, err
}
if valid {
return AuthenticateUserDN(userDN, form.Password, cfg)
}
return false, errors.New("User not found in LDAP")
}