horizoncd/horizon

View on GitHub
core/middleware/user/user.go

Summary

Maintainability
C
1 day
Test Coverage
// Copyright © 2023 Horizoncd.
//
// Licensed 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.
 
package user
 
import (
"bytes"
"context"
"crypto/hmac"
"crypto/md5"
"crypto/sha256"
"encoding/base64"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
 
"github.com/gin-gonic/gin"
"github.com/gorilla/sessions"
 
"github.com/horizoncd/horizon/core/common"
coreconfig "github.com/horizoncd/horizon/core/config"
herrors "github.com/horizoncd/horizon/core/errors"
"github.com/horizoncd/horizon/core/middleware"
userauth "github.com/horizoncd/horizon/pkg/authentication/user"
"github.com/horizoncd/horizon/pkg/config/authenticate"
perror "github.com/horizoncd/horizon/pkg/errors"
"github.com/horizoncd/horizon/pkg/param"
"github.com/horizoncd/horizon/pkg/server/response"
"github.com/horizoncd/horizon/pkg/server/rpcerror"
usermanager "github.com/horizoncd/horizon/pkg/user/manager"
"github.com/horizoncd/horizon/pkg/user/models"
"github.com/horizoncd/horizon/pkg/util/log"
)
 
const (
_timeGMT = "Mon, 02 Jan 2006 15:04:05 GMT"
 
HTTPHeaderOperator = "Operator"
HTTPHeaderSignature = "Signature"
HTTPHeaderContentMD5 = "Content-MD5"
HTTPHeaderContentType = "Content-Type"
HTTPHeaderDate = "Date"
 
NotAuthHeader = "X-OIDC-Redirect-To"
)
 
// Middleware check user is exists in db. If not, add user into db.
// Then attach a User object into context.
func Middleware(param *param.Param, store sessions.Store,
config *coreconfig.Config, skippers ...middleware.Skipper) gin.HandlerFunc {
return middleware.New(func(c *gin.Context) {
// 1. aksk auth if operator header exists
user, err := akskAuthn(c, config.AccessSecretKeys, param.UserMgr)
if err != nil {
response.AbortWithRPCError(c, rpcerror.ForbiddenError.WithErrMsg(err.Error()))
return
}
 
if user != nil {
c.Set(common.UserContextKey(), &userauth.DefaultInfo{
Name: user.Name,
FullName: user.FullName,
ID: user.ID,
Email: user.Email,
Admin: user.Admin,
})
c.Next()
return
}
 
// 2. token auth request ( get user by token)
if _, err := common.GetToken(c); err == nil {
c.Next()
return
}
 
session, err := store.Get(c.Request, common.CookieKeyAuth)
if err != nil {
response.Abort(c, http.StatusUnauthorized,
http.StatusText(http.StatusUnauthorized),
fmt.Sprintf("session is not found\n"+
"session name = %s\n, err = %v", common.CookieKeyAuth, err))
return
}
 
u := session.Values[common.SessionKeyAuthUser]
if user, ok := u.(*userauth.DefaultInfo); ok && user != nil {
// attach user to context
common.SetUser(c, user)
c.Next()
return
}
 
// default status code of response is 200,
// if status is not 200, that means it has been handled by other middleware,
// so just omit it.
if c.Writer.Status() != http.StatusOK ||
// if not login, call this to login
// if signed in, call this to link other api
c.Request.URL.Path == common.URLLoginCallback {
c.Next()
return
}
 
if c.Request.URL.Path == common.URLOauthAuthorization {
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s?redirect=%s",
common.URLFrontLogin, url.QueryEscape(c.Request.RequestURI)))
c.AbortWithStatus(http.StatusTemporaryRedirect)
return
}
 
c.Header(NotAuthHeader, "NotAuth")
response.AbortWithRPCError(c, rpcerror.Unauthorized.WithErrMsg("please login"))
}, skippers...)
}
 
Function `akskAuthn` has 66 lines of code (exceeds 50 allowed). Consider refactoring.
Function `akskAuthn` has 12 return statements (exceeds 4 allowed).
Function `akskAuthn` has a Cognitive Complexity of 21 (exceeds 20 allowed). Consider refactoring.
func akskAuthn(c *gin.Context, keys authenticate.KeysConfig, userMgr usermanager.Manager) (*models.User, error) {
r := c.Request
log.Infof(c, "request url path: %v", r.URL)
 
operator := r.Header.Get(HTTPHeaderOperator)
if operator == "" {
return nil, nil
}
 
date := r.Header.Get(HTTPHeaderDate)
if date == "" {
return nil, perror.Wrap(herrors.ErrParamInvalid, "Date Header is missing")
}
parsedTime, err := time.Parse(_timeGMT, date)
if err != nil {
return nil, perror.Wrap(herrors.ErrParamInvalid, "Invalid Date Header format")
}
now := time.Now()
duration := time.Minute * 10
if parsedTime.Before(now.Add(0-duration)) || parsedTime.After(now.Add(duration)) {
return nil, perror.Wrap(herrors.ErrParamInvalid, "The Date has expired")
}
 
if err := validatingContentMD5(c, r); err != nil {
return nil, err
}
 
signature := r.Header.Get(HTTPHeaderSignature)
if signature == "" {
return nil, perror.Wrapf(herrors.ErrParamInvalid, "%v Header is missing", HTTPHeaderSignature)
}
strs := strings.Split(signature, ":")
if len(strs) != 2 || strings.TrimSpace(strs[0]) == "" || strings.TrimSpace(strs[1]) == "" {
return nil, perror.Wrapf(herrors.ErrParamInvalid, "Invalid %v Header format", HTTPHeaderSignature)
}
 
var key *authenticate.Key
accessKey := strings.TrimSpace(strs[0])
secretKey := ""
for user, ks := range keys {
found := false
for i := range ks {
if ks[i].AccessKey == accessKey {
secretKey = ks[i].SecretKey
found = true
key = ks[i]
break
}
}
if found {
log.Infof(c, "the caller name: %v, operator: %v", user, operator)
break
}
}
if secretKey == "" {
return nil, perror.Wrapf(herrors.ErrParamInvalid, "invalid access key")
}
 
actualSignature := signature
expectSignature := SignRequest(c, r, accessKey, secretKey)
if actualSignature != expectSignature {
return nil, perror.Wrapf(herrors.ErrParamInvalid, "signature verify failed on %v Header", HTTPHeaderSignature)
}
 
u, err := userMgr.GetUserByIDP(c, operator, key.IDP)
if err != nil {
return nil, perror.Wrapf(herrors.NewErrGetFailed(herrors.UserInDB, "unauthorized"),
"failed to get user with email = %v and idp = %v: err = %v", operator, key.IDP, err)
}
if u == nil {
return nil, perror.Wrapf(herrors.NewErrNotFound(herrors.UserInDB, "unauthorized"),
"user with email = %v and idp = %v not found", operator, key.IDP)
}
 
return u, nil
}
 
Identical blocks of code found in 2 locations. Consider refactoring.
func validatingContentMD5(ctx context.Context, r *http.Request) error {
data, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Errorf(ctx, err.Error())
return err
}
log.Infof(ctx, "data: %v", string(data))
defer func() { _ = r.Body.Close() }()
r.Body = ioutil.NopCloser(bytes.NewReader(data))
 
if len(data) > 0 {
log.Infof(ctx, "request body: %v", string(data))
contentMD5 := r.Header.Get(HTTPHeaderContentMD5)
if contentMD5 == "" {
return fmt.Errorf("Content-MD5 Header is missing")
}
 
hash := md5.Sum(data)
buf := base64.StdEncoding.EncodeToString(hash[:])
if buf != contentMD5 {
return fmt.Errorf("Content-MD5 Header is invalid, content:%v, expect:%v, actual:%v",
string(data), buf, contentMD5)
}
}
return nil
}
 
Identical blocks of code found in 2 locations. Consider refactoring.
func SignRequest(ctx context.Context, request *http.Request, publicKey string, secretKey string) string {
/*
* StringToSign = HTTP-Verb + "\n" +
* PATH + "\n" +
* Content-MD5 + "\n" +
* Content-Type + "\n" +
* Date + "\n"
*/
var stringToSign string
stringToSign += request.Method + "\n"
if strings.HasPrefix(request.URL.Path, "/") {
stringToSign += "/" + url.QueryEscape(request.URL.Path[1:]) + "\n"
} else {
stringToSign += "/" + url.QueryEscape(request.URL.Path) + "\n"
}
stringToSign += request.Header.Get(HTTPHeaderContentMD5) + "\n"
stringToSign += request.Header.Get(HTTPHeaderContentType) + "\n"
stringToSign += request.Header.Get(HTTPHeaderDate) + "\n"
log.Debugf(ctx, "stringToSign:\n%v", stringToSign)
 
key := []byte(secretKey)
h := hmac.New(sha256.New, key)
_, err := h.Write([]byte(stringToSign))
if err != nil {
log.Errorf(ctx, "error: %v", err)
}
signature := base64.StdEncoding.EncodeToString(h.Sum(nil))
log.Debugf(ctx, "publicKey:%v, signature:\n%v", publicKey, signature)
 
return publicKey + ":" + signature
}