github.com/minio/console@v1.4.1/api/user_session.go (about)

     1  // This file is part of MinIO Console Server
     2  // Copyright (c) 2021 MinIO, Inc.
     3  //
     4  // This program is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Affero General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // This program is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    12  // GNU Affero General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Affero General Public License
    15  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package api
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"encoding/json"
    23  	"strconv"
    24  	"time"
    25  
    26  	policies "github.com/minio/console/api/policy"
    27  	"github.com/minio/madmin-go/v3"
    28  
    29  	jwtgo "github.com/golang-jwt/jwt/v4"
    30  	"github.com/minio/pkg/v3/policy/condition"
    31  
    32  	minioIAMPolicy "github.com/minio/pkg/v3/policy"
    33  
    34  	"github.com/go-openapi/runtime/middleware"
    35  	"github.com/minio/console/api/operations"
    36  	authApi "github.com/minio/console/api/operations/auth"
    37  	"github.com/minio/console/models"
    38  	"github.com/minio/console/pkg/auth/idp/oauth2"
    39  	"github.com/minio/console/pkg/auth/ldap"
    40  )
    41  
    42  type Conditions struct {
    43  	S3Prefix []string `json:"s3:prefix"`
    44  }
    45  
    46  func registerSessionHandlers(api *operations.ConsoleAPI) {
    47  	// session check
    48  	api.AuthSessionCheckHandler = authApi.SessionCheckHandlerFunc(func(params authApi.SessionCheckParams, session *models.Principal) middleware.Responder {
    49  		sessionResp, err := getSessionResponse(params.HTTPRequest.Context(), session)
    50  		if err != nil {
    51  			return authApi.NewSessionCheckDefault(err.Code).WithPayload(err.APIError)
    52  		}
    53  		return authApi.NewSessionCheckOK().WithPayload(sessionResp)
    54  	})
    55  }
    56  
    57  func getClaimsFromToken(sessionToken string) (map[string]interface{}, error) {
    58  	jp := new(jwtgo.Parser)
    59  	// nolint:staticcheck // ignore SA1019
    60  	jp.ValidMethods = []string{
    61  		"RS256", "RS384", "RS512", "ES256", "ES384", "ES512",
    62  		"RS3256", "RS3384", "RS3512", "ES3256", "ES3384", "ES3512",
    63  	}
    64  	var claims jwtgo.MapClaims
    65  	_, _, err := jp.ParseUnverified(sessionToken, &claims)
    66  	if err != nil {
    67  		return nil, err
    68  	}
    69  	return claims, nil
    70  }
    71  
    72  // getSessionResponse parse the token of the current session and returns a list of allowed actions to render in the UI
    73  func getSessionResponse(ctx context.Context, session *models.Principal) (*models.SessionResponse, *CodedAPIError) {
    74  	ctx, cancel := context.WithCancel(ctx)
    75  	defer cancel()
    76  
    77  	// serialize output
    78  	if session == nil {
    79  		return nil, ErrorWithContext(ctx, ErrInvalidSession)
    80  	}
    81  	tokenClaims, _ := getClaimsFromToken(session.STSSessionToken)
    82  	// initialize admin client
    83  	mAdminClient, err := NewMinioAdminClient(ctx, &models.Principal{
    84  		STSAccessKeyID:     session.STSAccessKeyID,
    85  		STSSecretAccessKey: session.STSSecretAccessKey,
    86  		STSSessionToken:    session.STSSessionToken,
    87  	})
    88  	if err != nil {
    89  		return nil, ErrorWithContext(ctx, err, ErrInvalidSession)
    90  	}
    91  	userAdminClient := AdminClient{Client: mAdminClient}
    92  	// Obtain the current policy assigned to this user
    93  	// necessary for generating the list of allowed endpoints
    94  	accountInfo, err := getAccountInfo(ctx, userAdminClient)
    95  	if err != nil {
    96  		return nil, ErrorWithContext(ctx, err, ErrInvalidSession)
    97  	}
    98  	erasure := accountInfo.Server.Type == madmin.Erasure
    99  	rawPolicy := policies.ReplacePolicyVariables(tokenClaims, accountInfo)
   100  	policy, err := minioIAMPolicy.ParseConfig(bytes.NewReader(rawPolicy))
   101  	if err != nil {
   102  		return nil, ErrorWithContext(ctx, err, ErrInvalidSession)
   103  	}
   104  	currTime := time.Now().UTC()
   105  
   106  	customStyles := session.CustomStyleOb
   107  	// This actions will be global, meaning has to be attached to all resources
   108  	conditionValues := map[string][]string{
   109  		condition.AWSUsername.Name(): {session.AccountAccessKey},
   110  		// All calls to MinIO from console use temporary credentials.
   111  		condition.AWSPrincipalType.Name():   {"AssumeRole"},
   112  		condition.AWSSecureTransport.Name(): {strconv.FormatBool(getMinIOEndpointIsSecure())},
   113  		condition.AWSCurrentTime.Name():     {currTime.Format(time.RFC3339)},
   114  		condition.AWSEpochTime.Name():       {strconv.FormatInt(currTime.Unix(), 10)},
   115  
   116  		// All calls from console are signature v4.
   117  		condition.S3SignatureVersion.Name(): {"AWS4-HMAC-SHA256"},
   118  		// All calls from console are signature v4.
   119  		condition.S3AuthType.Name(): {"REST-HEADER"},
   120  		// This is usually empty, may be set some times (rare).
   121  		condition.S3LocationConstraint.Name(): {GetMinIORegion()},
   122  	}
   123  
   124  	claims, err := getClaimsFromToken(session.STSSessionToken)
   125  	if err != nil {
   126  		return nil, ErrorWithContext(ctx, err, ErrInvalidSession)
   127  	}
   128  
   129  	// Support all LDAP, JWT variables
   130  	for k, v := range claims {
   131  		vstr, ok := v.(string)
   132  		if !ok {
   133  			// skip all non-strings
   134  			continue
   135  		}
   136  		// store all claims from sessionToken
   137  		conditionValues[k] = []string{vstr}
   138  	}
   139  
   140  	defaultActions := policy.IsAllowedActions("", "", conditionValues)
   141  
   142  	// Allow Create Access Key when admin:CreateServiceAccount is provided with a condition
   143  	for _, statement := range policy.Statements {
   144  		if statement.Effect == "Deny" && len(statement.Conditions) > 0 &&
   145  			statement.Actions.Contains(minioIAMPolicy.CreateServiceAccountAdminAction) {
   146  			defaultActions.Add(minioIAMPolicy.Action(minioIAMPolicy.CreateServiceAccountAdminAction))
   147  		}
   148  	}
   149  
   150  	permissions := map[string]minioIAMPolicy.ActionSet{
   151  		ConsoleResourceName: defaultActions,
   152  	}
   153  	deniedActions := map[string]minioIAMPolicy.ActionSet{}
   154  
   155  	var allowResources []*models.PermissionResource
   156  
   157  	for _, statement := range policy.Statements {
   158  		for _, resource := range statement.Resources.ToSlice() {
   159  			resourceName := resource.String()
   160  			statementActions := statement.Actions.ToSlice()
   161  			var prefixes []string
   162  
   163  			if statement.Effect == "Allow" {
   164  				// check if val are denied before adding them to the map
   165  				var allowedActions []minioIAMPolicy.Action
   166  				if dActions, ok := deniedActions[resourceName]; ok {
   167  					for _, action := range statementActions {
   168  						if len(dActions.Intersection(minioIAMPolicy.NewActionSet(action))) == 0 {
   169  							// It's ok to allow this action
   170  							allowedActions = append(allowedActions, action)
   171  						}
   172  					}
   173  				} else {
   174  					allowedActions = statementActions
   175  				}
   176  
   177  				// Add validated actions
   178  				if resourceActions, ok := permissions[resourceName]; ok {
   179  					mergedActions := append(resourceActions.ToSlice(), allowedActions...)
   180  					permissions[resourceName] = minioIAMPolicy.NewActionSet(mergedActions...)
   181  				} else {
   182  					mergedActions := append(defaultActions.ToSlice(), allowedActions...)
   183  					permissions[resourceName] = minioIAMPolicy.NewActionSet(mergedActions...)
   184  				}
   185  
   186  				// Allow Permissions request
   187  				conditions, err := statement.Conditions.MarshalJSON()
   188  				if err != nil {
   189  					return nil, ErrorWithContext(ctx, err)
   190  				}
   191  
   192  				var wrapper map[string]Conditions
   193  
   194  				if err := json.Unmarshal(conditions, &wrapper); err != nil {
   195  					return nil, ErrorWithContext(ctx, err)
   196  				}
   197  
   198  				for condition, elements := range wrapper {
   199  					prefixes = elements.S3Prefix
   200  
   201  					resourceElement := models.PermissionResource{
   202  						Resource:          resourceName,
   203  						Prefixes:          prefixes,
   204  						ConditionOperator: condition,
   205  					}
   206  
   207  					allowResources = append(allowResources, &resourceElement)
   208  				}
   209  			} else {
   210  				// Add new banned actions to the map
   211  				if resourceActions, ok := deniedActions[resourceName]; ok {
   212  					mergedActions := append(resourceActions.ToSlice(), statementActions...)
   213  					deniedActions[resourceName] = minioIAMPolicy.NewActionSet(mergedActions...)
   214  				} else {
   215  					deniedActions[resourceName] = statement.Actions
   216  				}
   217  				// Remove existing val from key if necessary
   218  				if currentResourceActions, ok := permissions[resourceName]; ok {
   219  					var newAllowedActions []minioIAMPolicy.Action
   220  					for _, action := range currentResourceActions.ToSlice() {
   221  						if len(deniedActions[resourceName].Intersection(minioIAMPolicy.NewActionSet(action))) == 0 {
   222  							// It's ok to allow this action
   223  							newAllowedActions = append(newAllowedActions, action)
   224  						}
   225  					}
   226  					permissions[resourceName] = minioIAMPolicy.NewActionSet(newAllowedActions...)
   227  				}
   228  			}
   229  		}
   230  	}
   231  	resourcePermissions := map[string][]string{}
   232  	for key, val := range permissions {
   233  		var resourceActions []string
   234  		for _, action := range val.ToSlice() {
   235  			resourceActions = append(resourceActions, string(action))
   236  		}
   237  		resourcePermissions[key] = resourceActions
   238  
   239  	}
   240  	serializedPolicy, err := json.Marshal(policy)
   241  	if err != nil {
   242  		return nil, ErrorWithContext(ctx, err, ErrInvalidSession)
   243  	}
   244  	var sessionPolicy *models.IamPolicy
   245  	err = json.Unmarshal(serializedPolicy, &sessionPolicy)
   246  	if err != nil {
   247  		return nil, ErrorWithContext(ctx, err)
   248  	}
   249  
   250  	// environment constants
   251  	var envConstants models.EnvironmentConstants
   252  
   253  	envConstants.MaxConcurrentUploads = getMaxConcurrentUploadsLimit()
   254  	envConstants.MaxConcurrentDownloads = getMaxConcurrentDownloadsLimit()
   255  
   256  	sessionResp := &models.SessionResponse{
   257  		Features:        getListOfEnabledFeatures(ctx, userAdminClient, session),
   258  		Status:          models.SessionResponseStatusOk,
   259  		Operator:        false,
   260  		DistributedMode: erasure,
   261  		Permissions:     resourcePermissions,
   262  		AllowResources:  allowResources,
   263  		CustomStyles:    customStyles,
   264  		EnvConstants:    &envConstants,
   265  		ServerEndPoint:  getMinIOServer(),
   266  	}
   267  	return sessionResp, nil
   268  }
   269  
   270  // getListOfEnabledFeatures returns a list of features
   271  func getListOfEnabledFeatures(ctx context.Context, minioClient MinioAdmin, session *models.Principal) []string {
   272  	features := []string{}
   273  	logSearchURL := getLogSearchURL()
   274  	oidcEnabled := oauth2.IsIDPEnabled()
   275  	ldapEnabled := ldap.GetLDAPEnabled()
   276  
   277  	if logSearchURL != "" {
   278  		features = append(features, "log-search")
   279  	}
   280  	if oidcEnabled {
   281  		features = append(features, "oidc-idp", "external-idp")
   282  	}
   283  	if ldapEnabled {
   284  		features = append(features, "ldap-idp", "external-idp")
   285  	}
   286  
   287  	if session.Hm {
   288  		features = append(features, "hide-menu")
   289  	}
   290  	if session.Ob {
   291  		features = append(features, "object-browser-only")
   292  	}
   293  	if minioClient != nil {
   294  		_, err := minioClient.kmsStatus(ctx)
   295  		if err == nil {
   296  			features = append(features, "kms")
   297  		}
   298  	}
   299  
   300  	return features
   301  }