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 }