Files
rancher/pkg/auth/providers/googleoauth/goauth_provider.go

430 lines
15 KiB
Go

package googleoauth
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
v32 "github.com/rancher/rancher/pkg/apis/management.cattle.io/v3"
"github.com/mitchellh/mapstructure"
"github.com/rancher/norman/httperror"
"github.com/rancher/norman/types"
"github.com/rancher/norman/types/convert"
"github.com/rancher/rancher/pkg/auth/providers/common"
"github.com/rancher/rancher/pkg/auth/tokens"
client "github.com/rancher/rancher/pkg/client/generated/management/v3"
publicclient "github.com/rancher/rancher/pkg/client/generated/management/v3public"
corev1 "github.com/rancher/rancher/pkg/generated/norman/core/v1"
v3 "github.com/rancher/rancher/pkg/generated/norman/management.cattle.io/v3"
"github.com/rancher/rancher/pkg/types/config"
"github.com/rancher/rancher/pkg/user"
"github.com/sirupsen/logrus"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
admin "google.golang.org/api/admin/directory/v1"
"google.golang.org/api/googleapi"
"google.golang.org/api/option"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
const (
Name = "googleoauth"
userType = "user"
groupType = "group"
domainPublicViewType = "domain_public"
)
var scopes = []string{"openid", "profile", "email", admin.AdminDirectoryUserReadonlyScope, admin.AdminDirectoryGroupReadonlyScope}
type googleOauthProvider struct {
authConfigs v3.AuthConfigInterface
secrets corev1.SecretInterface
goauthClient *GClient
userMGR user.Manager
tokenMGR *tokens.Manager
ctx context.Context
}
func Configure(ctx context.Context, mgmtCtx *config.ScaledContext, userMGR user.Manager, tokenMGR *tokens.Manager) common.AuthProvider {
gClient := GClient{
httpClient: &http.Client{
Timeout: time.Second * 30,
},
}
return &googleOauthProvider{
ctx: ctx,
authConfigs: mgmtCtx.Management.AuthConfigs(""),
secrets: mgmtCtx.Core.Secrets(""),
goauthClient: &gClient,
userMGR: userMGR,
tokenMGR: tokenMGR,
}
}
func (g *googleOauthProvider) AuthenticateUser(ctx context.Context, input interface{}) (v3.Principal, []v3.Principal, string, error) {
login, ok := input.(*v32.GoogleOauthLogin)
if !ok {
return v3.Principal{}, nil, "", fmt.Errorf("unexpected input type")
}
return g.loginUser(ctx, login, nil, false)
}
// loginUser takes as input the code; gets access_token and refresh_token in exhange; uses access_token to get user info
// and groups (if allowed); and returns the user and group principals and oauth token
func (g *googleOauthProvider) loginUser(c context.Context, googleOAuthCredential *v32.GoogleOauthLogin, config *v32.GoogleOauthConfig, testAndEnableAction bool) (v3.Principal, []v3.Principal, string, error) {
var groupPrincipals []v3.Principal
var userPrincipal v3.Principal
var err error
if config == nil {
config, err = g.getGoogleOAuthConfigCR()
if err != nil {
return userPrincipal, groupPrincipals, "", err
}
}
logrus.Debugf("[Google OAuth] loginuser: Using code to get oauth token")
securityCode := googleOAuthCredential.Code
oauth2Config, err := google.ConfigFromJSON([]byte(config.OauthCredential), scopes...)
if err != nil {
return userPrincipal, groupPrincipals, "", err
}
// Exchange the code for oauthToken
gOAuthToken, err := oauth2Config.Exchange(c, securityCode)
if err != nil {
return userPrincipal, groupPrincipals, "", err
}
logrus.Debugf("[Google OAuth] loginuser: Exchanged code for oauth token")
// init the admin directory service
adminSvc, err := g.getDirectoryService(c, config.AdminEmail, []byte(config.ServiceAccountCredential), oauth2Config.TokenSource(c, gOAuthToken))
if err != nil {
return userPrincipal, groupPrincipals, "", err
}
userPrincipal, groupPrincipals, err = g.getUserInfoAndGroups(adminSvc, gOAuthToken, config, testAndEnableAction)
if err != nil {
return userPrincipal, groupPrincipals, "", err
}
logrus.Debugf("[Google OAuth] loginuser: Checking user's access to Rancher")
allowed, err := g.userMGR.CheckAccess(config.AccessMode, config.AllowedPrincipalIDs, userPrincipal.Name, groupPrincipals)
if err != nil {
return userPrincipal, groupPrincipals, "", err
}
if !allowed {
return userPrincipal, groupPrincipals, "", httperror.NewAPIError(httperror.Unauthorized, "unauthorized")
}
// save entire oauthToken because it contains refresh_token and token expiry time
// oauth2.TokenSource used in getDirectoryService uses all these fields to auto renew the access token (Ref: https://github.com/golang/oauth2/blob/aaccbc9213b0974828f81aaac109d194880e3014/oauth2.go#L235)
oauthToken, err := json.Marshal(gOAuthToken)
if err != nil {
return userPrincipal, groupPrincipals, "", err
}
logrus.Debugf("[Google OAuth] loginuser: Returning principals and marshaled oauth token")
return userPrincipal, groupPrincipals, string(oauthToken), nil
}
func (g *googleOauthProvider) SearchPrincipals(searchKey, principalType string, token v3.Token) ([]v3.Principal, error) {
var principals []v3.Principal
var err error
config, err := g.getGoogleOAuthConfigCR()
if err != nil {
return principals, err
}
storedOauthToken, err := g.tokenMGR.GetSecret(token.UserID, token.AuthProvider, []*v3.Token{&token})
if err != nil && !apierrors.IsNotFound(err) {
return nil, err
}
logrus.Debugf("[Google OAuth] SearchPrincipals: Retrieved stored oauth token")
adminSvc, err := g.getdirectoryServiceFromStoredToken(storedOauthToken, config)
if err != nil {
return principals, err
}
logrus.Debugf("[Google OAuth] SearchPrincipals: Initialized dir svc with stored oauth token")
accounts, err := g.searchPrincipals(adminSvc, searchKey, principalType, config)
if err != nil {
return principals, err
}
for _, acc := range accounts {
principals = append(principals, g.toPrincipal(acc.Type, acc, &token))
}
logrus.Debugf("[Google OAuth] SearchPrincipals: Returning principals")
return principals, nil
}
func (g *googleOauthProvider) GetPrincipal(principalID string, token v3.Token) (v3.Principal, error) {
var principal v3.Principal
config, err := g.getGoogleOAuthConfigCR()
if err != nil {
return principal, err
}
storedOauthToken, err := g.tokenMGR.GetSecret(token.UserID, token.AuthProvider, []*v3.Token{&token})
if err != nil {
if !apierrors.IsNotFound(err) {
return principal, err
}
}
logrus.Debugf("[Google OAuth] GetPrincipal: Retrieved stored oauth token")
adminSvc, err := g.getdirectoryServiceFromStoredToken(storedOauthToken, config)
if err != nil {
return principal, err
}
logrus.Debugf("[Google OAuth] GetPrincipal: Initialized dir svc with stored oauth token")
externalID, principalType, err := getUIDFromPrincipalID(principalID)
if err != nil {
return principal, err
}
logrus.Debugf("[Google OAuth] GetPrincipal: Parsed principalID")
switch principalType {
case userType:
user, err := adminSvc.Users.Get(externalID).Do()
if err != nil {
if config.ServiceAccountCredential == "" {
// used client creds, try get again with viewType=domain_public
if gErr, ok := err.(*googleapi.Error); ok && gErr.Code == http.StatusForbidden {
user, err = adminSvc.Users.Get(externalID).ViewType(domainPublicViewType).Do()
if err != nil {
return principal, err
}
} else {
return principal, err
}
} else {
return principal, err
}
}
acc := Account{
SubjectUniqueID: user.Id,
Email: user.PrimaryEmail,
PictureURL: user.ThumbnailPhotoUrl,
Type: userType,
}
if user.Name != nil {
acc.Name = user.Name.FullName
acc.GivenName = user.Name.GivenName
acc.FamilyName = user.Name.FamilyName
}
return g.toPrincipal(userType, acc, &token), nil
case groupType:
group, err := adminSvc.Groups.Get(externalID).Do()
if err != nil {
if config.ServiceAccountCredential == "" {
// used client creds, getting group for non-admin might fail with forbidden, if that's the case don't throw
// error
if gErr, ok := err.(*googleapi.Error); ok && gErr.Code == http.StatusForbidden {
return principal, nil
}
}
return principal, err
}
return g.toPrincipal(groupType, Account{SubjectUniqueID: group.Id, Email: group.Email, Name: group.Name}, &token), nil
default:
return principal, fmt.Errorf("cannot get the google account due to invalid externalIDType %v", principalType)
}
}
func (g *googleOauthProvider) GetName() string {
return Name
}
func (g *googleOauthProvider) CustomizeSchema(schema *types.Schema) {
schema.ActionHandler = g.actionHandler
schema.Formatter = g.formatter
}
func (g *googleOauthProvider) TransformToAuthProvider(authConfig map[string]interface{}) (map[string]interface{}, error) {
p := common.TransformToAuthProvider(authConfig)
val, err := g.formGoogleOAuthRedirectURLFromMap(authConfig)
if err != nil {
return nil, err
}
p[publicclient.GoogleOAuthProviderFieldRedirectURL] = val
return p, nil
}
func (g *googleOauthProvider) RefetchGroupPrincipals(principalID string, secret string) ([]v3.Principal, error) {
var principals []v3.Principal
config, err := g.getGoogleOAuthConfigCR()
if err != nil {
return principals, err
}
adminSvc, err := g.getdirectoryServiceFromStoredToken(secret, config)
if err != nil {
return principals, err
}
logrus.Debugf("[Google OAuth] RefetchGroupPrincipals: Initialized dir svc with stored oauth token")
externalID, _, err := getUIDFromPrincipalID(principalID)
if err != nil {
return principals, err
}
logrus.Debugf("[Google OAuth] GetPrincipal: Parsed principalID")
groupPrincipals, err := g.getGroupsUserBelongsTo(adminSvc, externalID, config.Hostname, config)
if err != nil {
return principals, err
}
if !config.NestedGroupMembershipEnabled {
return groupPrincipals, nil
}
return g.fetchParentGroups(config, groupPrincipals, adminSvc, config.Hostname)
}
func (g *googleOauthProvider) CanAccessWithGroupProviders(userPrincipalID string, groupPrincipals []v3.Principal) (bool, error) {
config, err := g.getGoogleOAuthConfigCR()
if err != nil {
logrus.Errorf("Error fetching google OAuth config: %v", err)
return false, err
}
allowed, err := g.userMGR.CheckAccess(config.AccessMode, config.AllowedPrincipalIDs, userPrincipalID, groupPrincipals)
if err != nil {
return false, err
}
return allowed, nil
}
func (g *googleOauthProvider) getGoogleOAuthConfigCR() (*v32.GoogleOauthConfig, error) {
authConfigObj, err := g.authConfigs.ObjectClient().UnstructuredClient().Get(Name, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("failed to retrieve GoogleOAuthConfig, error: %v", err)
}
u, ok := authConfigObj.(runtime.Unstructured)
if !ok {
return nil, fmt.Errorf("failed to retrieve GoogleOAuthConfig, cannot read k8s Unstructured data")
}
storedGoogleOAuthConfigMap := u.UnstructuredContent()
storedGoogleOAuthConfig := &v32.GoogleOauthConfig{}
mapstructure.Decode(storedGoogleOAuthConfigMap, storedGoogleOAuthConfig)
metadataMap, ok := storedGoogleOAuthConfigMap["metadata"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("failed to retrieve GoogleOAuthConfig metadata, cannot read k8s Unstructured data")
}
typemeta := &metav1.ObjectMeta{}
mapstructure.Decode(metadataMap, typemeta)
storedGoogleOAuthConfig.ObjectMeta = *typemeta
if storedGoogleOAuthConfig.OauthCredential != "" {
value, err := common.ReadFromSecret(g.secrets, storedGoogleOAuthConfig.OauthCredential, strings.ToLower(client.GoogleOauthConfigFieldOauthCredential))
if err != nil {
return nil, err
}
storedGoogleOAuthConfig.OauthCredential = value
}
if storedGoogleOAuthConfig.ServiceAccountCredential != "" {
value, err := common.ReadFromSecret(g.secrets, storedGoogleOAuthConfig.ServiceAccountCredential, strings.ToLower(client.GoogleOauthConfigFieldServiceAccountCredential))
if err != nil {
return nil, err
}
storedGoogleOAuthConfig.ServiceAccountCredential = value
}
return storedGoogleOAuthConfig, nil
}
func (g *googleOauthProvider) saveGoogleOAuthConfigCR(config *v32.GoogleOauthConfig) error {
storedGoogleOAuthConfig, err := g.getGoogleOAuthConfigCR()
if err != nil {
return err
}
config.APIVersion = "management.cattle.io/v3"
config.Kind = v3.AuthConfigGroupVersionKind.Kind
config.Type = client.GoogleOauthConfigType
config.ObjectMeta = storedGoogleOAuthConfig.ObjectMeta
secretInfo := convert.ToString(config.OauthCredential)
field := strings.ToLower(client.GoogleOauthConfigFieldOauthCredential)
if err := common.CreateOrUpdateSecrets(g.secrets, secretInfo, field, strings.ToLower(config.Type)); err != nil {
return err
}
config.OauthCredential = common.GetName(config.Type, field)
if config.ServiceAccountCredential != "" {
secretInfo = convert.ToString(config.ServiceAccountCredential)
field = strings.ToLower(client.GoogleOauthConfigFieldServiceAccountCredential)
if err := common.CreateOrUpdateSecrets(g.secrets, secretInfo, field, strings.ToLower(config.Type)); err != nil {
return err
}
config.ServiceAccountCredential = common.GetName(config.Type, field)
}
_, err = g.authConfigs.ObjectClient().Update(config.ObjectMeta.Name, config)
if err != nil {
return err
}
return nil
}
func (g *googleOauthProvider) toPrincipal(principalType string, acct Account, token *v3.Token) v3.Principal {
displayName := acct.Name
if displayName == "" {
displayName = acct.Email
}
princ := v3.Principal{
ObjectMeta: metav1.ObjectMeta{Name: Name + "_" + principalType + "://" + acct.SubjectUniqueID},
DisplayName: displayName,
LoginName: acct.Email,
Provider: Name,
Me: false,
ProfilePicture: acct.PictureURL,
}
if principalType == userType {
princ.PrincipalType = "user"
if token != nil {
princ.Me = g.isThisUserMe(token.UserPrincipal, princ)
}
} else {
princ.PrincipalType = "group"
if token != nil {
princ.MemberOf = g.tokenMGR.IsMemberOf(*token, princ)
}
}
return princ
}
func (g *googleOauthProvider) isThisUserMe(me v3.Principal, other v3.Principal) bool {
if me.ObjectMeta.Name == other.ObjectMeta.Name && me.LoginName == other.LoginName && me.PrincipalType == other.PrincipalType {
return true
}
return false
}
func (g *googleOauthProvider) getDirectoryService(ctx context.Context, userEmail string, jsonCredentials []byte, accessTokenSource oauth2.TokenSource) (*admin.Service, error) {
if userEmail != "" && len(jsonCredentials) > 0 {
// oauth golang library performs all these steps: https://developers.google.com/identity/protocols/OAuth2ServiceAccount#jwt-auth
// using JWTConfigFromJSON method
config, err := google.JWTConfigFromJSON(jsonCredentials, admin.AdminDirectoryUserReadonlyScope, admin.AdminDirectoryGroupReadonlyScope)
if err != nil {
logrus.Errorf("[Google OAuth] error unmarshaling service account creds: %v", err)
return nil, fmt.Errorf("invalid Service Account Credentials provided")
}
config.Subject = userEmail
srv, err := admin.NewService(ctx, option.WithTokenSource(config.TokenSource(ctx)))
if err != nil {
logrus.Errorf("[Google OAuth] error generating tokenSource for service account creds: %v", err)
return nil, fmt.Errorf("invalid Service Account Credentials provided")
}
return srv, nil
}
// client oauth creds are used
srv, err := admin.NewService(ctx, option.WithTokenSource(accessTokenSource))
if err != nil {
return nil, err
}
return srv, nil
}