github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/pkg/auth/auth.go (about)

     1  /*This file is part of kuberpult.
     2  
     3  Kuberpult is free software: you can redistribute it and/or modify
     4  it under the terms of the Expat(MIT) License as published by
     5  the Free Software Foundation.
     6  
     7  Kuberpult is distributed in the hope that it will be useful,
     8  but WITHOUT ANY WARRANTY; without even the implied warranty of
     9  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    10  MIT License for more details.
    11  
    12  You should have received a copy of the MIT License
    13  along with kuberpult. If not, see <https://directory.fsf.org/wiki/License:Expat>.
    14  
    15  Copyright 2023 freiheit.com*/
    16  
    17  package auth
    18  
    19  import (
    20  	"context"
    21  	"encoding/base64"
    22  	"errors"
    23  	"fmt"
    24  	"net/http"
    25  
    26  	"github.com/freiheit-com/kuberpult/pkg/grpc"
    27  	"github.com/freiheit-com/kuberpult/pkg/logger"
    28  	"google.golang.org/grpc/metadata"
    29  )
    30  
    31  type ctxMarker struct{}
    32  
    33  var (
    34  	ctxMarkerKey = &ctxMarker{}
    35  )
    36  
    37  /*
    38  The frontend-service now defines the default author for git commits.
    39  The frontend-service also allows overwriting the default values, see function `getRequestAuthorFromGoogleIAP`.
    40  The cd-service generally expects these headers, either in the grpc context or the http headers.
    41  */
    42  const (
    43  	HeaderUserName  = "author-name"
    44  	HeaderUserEmail = "author-email"
    45  	HeaderUserRole  = "author-role"
    46  )
    47  
    48  func Encode64(s string) string {
    49  	return base64.StdEncoding.EncodeToString([]byte(s))
    50  }
    51  
    52  func Decode64(s string) (string, error) {
    53  	b, err := base64.StdEncoding.DecodeString(s)
    54  	return string(b), err
    55  }
    56  
    57  // ReadUserFromContext returns a user from the ctx or an error if none was found, or it is invalid
    58  func ReadUserFromContext(ctx context.Context) (*User, error) {
    59  	u, ok := ctx.Value(ctxMarkerKey).(*User)
    60  	if !ok || u == nil {
    61  		return nil, grpc.InternalError(ctx, errors.New("could not read user from context"))
    62  	}
    63  	return u, nil
    64  }
    65  
    66  // WriteUserToContext should be used in both frontend-service and cd-service.
    67  // WriteUserToContext adds the User to the context for extraction later.
    68  // The user must not be nil.
    69  // Returning the new context that has been created.
    70  func WriteUserToContext(ctx context.Context, u User) context.Context {
    71  	return context.WithValue(ctx, ctxMarkerKey, &u)
    72  }
    73  
    74  func WriteUserToGrpcContext(ctx context.Context, u User) context.Context {
    75  	return metadata.AppendToOutgoingContext(ctx, HeaderUserEmail, Encode64(u.Email), HeaderUserName, Encode64(u.Name))
    76  }
    77  
    78  // WriteUserRoleToGrpcContext adds the user role to the GRPC context.
    79  // Only used when RBAC is enabled.
    80  func WriteUserRoleToGrpcContext(ctx context.Context, userRole string) context.Context {
    81  	return metadata.AppendToOutgoingContext(ctx, HeaderUserRole, Encode64(userRole))
    82  }
    83  
    84  type GrpcContextReader interface {
    85  	ReadUserFromGrpcContext(ctx context.Context) (*User, error)
    86  }
    87  
    88  type DexGrpcContextReader struct {
    89  	DexEnabled bool
    90  }
    91  
    92  type DummyGrpcContextReader struct {
    93  	Role string
    94  }
    95  
    96  func (x *DummyGrpcContextReader) ReadUserFromGrpcContext(ctx context.Context) (*User, error) {
    97  	user := &User{
    98  		Email: "dummyMail@example.com",
    99  		Name:  "userName",
   100  		DexAuthContext: &DexAuthContext{
   101  			Role: x.Role,
   102  		},
   103  	}
   104  	return user, nil
   105  }
   106  
   107  // ReadUserFromGrpcContext should only be used in the cd-service.
   108  // ReadUserFromGrpcContext takes the User from middleware (context).
   109  // It returns a User or an error if the user is not found.
   110  func (x *DexGrpcContextReader) ReadUserFromGrpcContext(ctx context.Context) (*User, error) {
   111  	md, ok := metadata.FromIncomingContext(ctx)
   112  	if !ok {
   113  		return nil, grpc.AuthError(ctx, errors.New("could not retrieve metadata context with git author in grpc context"))
   114  	}
   115  	originalEmailArr := md.Get(HeaderUserEmail)
   116  	if len(originalEmailArr) != 1 {
   117  		return nil, grpc.AuthError(ctx, fmt.Errorf("did not find exactly 1 author-email in grpc context: %+v", originalEmailArr))
   118  	}
   119  	originalEmail := originalEmailArr[0]
   120  	userMail, err := Decode64(originalEmail)
   121  	if err != nil {
   122  		return nil, grpc.AuthError(ctx, fmt.Errorf("extract: non-base64 in author-email in grpc context %s", originalEmail))
   123  	}
   124  	originalNameArr := md.Get(HeaderUserName)
   125  	if len(originalNameArr) != 1 {
   126  		return nil, grpc.AuthError(ctx, fmt.Errorf("did not find exactly 1 author-name in grpc context %+v", originalNameArr))
   127  	}
   128  	originalName := originalNameArr[0]
   129  	userName, err := Decode64(originalName)
   130  	if err != nil {
   131  		return nil, grpc.AuthError(ctx, fmt.Errorf("extract: non-base64 in author-username in grpc context %s", userName))
   132  	}
   133  	logger.FromContext(ctx).Info(fmt.Sprintf("Extract: original mail %s. Decoded: %s", originalEmail, userMail))
   134  	logger.FromContext(ctx).Info(fmt.Sprintf("Extract: original name %s. Decoded: %s", originalName, userName))
   135  	u := &User{
   136  		DexAuthContext: nil,
   137  		Email:          userMail,
   138  		Name:           userName,
   139  	}
   140  	if u.Email == "" || u.Name == "" {
   141  		return nil, grpc.AuthError(ctx, errors.New("email and name in grpc context cannot both be empty"))
   142  	}
   143  	// RBAC Role of the user. only mandatory if DEX is enabled.
   144  	if x.DexEnabled {
   145  		rolesInHeader := md.Get(HeaderUserRole)
   146  		if len(rolesInHeader) == 0 {
   147  			return nil, grpc.AuthError(ctx, fmt.Errorf("extract: role undefined but dex is enabled"))
   148  		}
   149  		userRole, err := Decode64(rolesInHeader[0])
   150  		if err != nil {
   151  			return nil, grpc.AuthError(ctx, fmt.Errorf("extract: non-base64 in author-role in grpc context %s", userRole))
   152  		}
   153  		u.DexAuthContext = &DexAuthContext{
   154  			Role: userRole,
   155  		}
   156  	}
   157  	return u, nil
   158  }
   159  
   160  // ReadUserFromHttpHeader should only be used in the cd-service.
   161  // ReadUserFromHttpHeader takes the User from the http request.
   162  // It returns a User or an error if the user is not found.
   163  func ReadUserFromHttpHeader(ctx context.Context, r *http.Request) (*User, error) {
   164  	headerEmail64 := r.Header.Get(HeaderUserEmail)
   165  	headerEmail, err := Decode64(headerEmail64)
   166  	if err != nil {
   167  		return nil, grpc.AuthError(ctx, fmt.Errorf("ExtractUserHttp: invalid data in email: '%s'", headerEmail64))
   168  	}
   169  	headerName64 := r.Header.Get(HeaderUserName)
   170  	headerName, err := Decode64(headerName64)
   171  	if err != nil {
   172  		return nil, grpc.AuthError(ctx, fmt.Errorf("ExtractUserHttp: invalid data in name: '%s'", headerName64))
   173  	}
   174  	headerRole64 := r.Header.Get(HeaderUserRole)
   175  	headerRole, err := Decode64(headerRole64)
   176  	if err != nil {
   177  		return nil, grpc.AuthError(ctx, fmt.Errorf("ExtractUserHttp: invalid data in role: '%s'", headerRole64))
   178  	}
   179  
   180  	if headerName != "" && headerEmail != "" {
   181  		return &User{
   182  			Email: headerEmail,
   183  			Name:  headerName,
   184  			DexAuthContext: &DexAuthContext{
   185  				Role: headerRole,
   186  			},
   187  		}, nil
   188  	}
   189  	return nil, nil // no user, but the user is not always necessary
   190  }
   191  
   192  // WriteUserToHttpHeader should only be used in the frontend-service
   193  // WriteUserToHttpHeader writes the user into http headers
   194  // it is used for requests like /release which are delegated from frontend-service to cd-service
   195  func WriteUserToHttpHeader(r *http.Request, user User) {
   196  	r.Header.Set(HeaderUserName, Encode64(user.Name))
   197  	r.Header.Set(HeaderUserEmail, Encode64(user.Email))
   198  }
   199  
   200  // WriteUserRoleToHttpHeader should only be used in the frontend-service
   201  // WriteUserRoleToHttpHeader writes the user role into http headers
   202  // it is used for requests like /release and managing locks which are delegated from frontend-service to cd-service
   203  func WriteUserRoleToHttpHeader(r *http.Request, role string) {
   204  	r.Header.Set(HeaderUserRole, Encode64(role))
   205  }
   206  
   207  func GetUserOrDefault(u *User, defaultUser User) User {
   208  	var userAdapted = User{
   209  		DexAuthContext: nil,
   210  		Email:          defaultUser.Email,
   211  		Name:           defaultUser.Name,
   212  	}
   213  	if u != nil && u.Email != "" {
   214  		userAdapted.Email = u.Email
   215  		// if no username was specified, use email as username
   216  		if u.Name == "" {
   217  			userAdapted.Name = u.Email
   218  		} else {
   219  			userAdapted.Name = u.Name
   220  		}
   221  	}
   222  	if u != nil && u.DexAuthContext != nil {
   223  		userAdapted.DexAuthContext = u.DexAuthContext
   224  	} else {
   225  		userAdapted.DexAuthContext = defaultUser.DexAuthContext
   226  	}
   227  	return userAdapted
   228  }
   229  
   230  type User struct {
   231  	Email string
   232  	Name  string
   233  	// Optional. User role, only used if RBAC is enabled.
   234  	DexAuthContext *DexAuthContext
   235  }