Replace aws:username, jwt: and ldap: policy variables in session policies (#1828)

* Replace username variable in session policies

Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
This commit is contained in:
Daniel Valdivia
2022-04-11 20:01:49 -07:00
committed by GitHub
parent dc5b1963ae
commit 6e6aab580c
8 changed files with 348 additions and 17 deletions

View File

@@ -0,0 +1,87 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package policy
import (
"bytes"
"encoding/json"
"fmt"
"github.com/minio/madmin-go"
)
// ReplacePolicyVariables replaces known variables from policies with known values
func ReplacePolicyVariables(claims map[string]interface{}, accountInfo *madmin.AccountInfo) json.RawMessage {
// AWS Variables
rawPolicy := bytes.ReplaceAll(accountInfo.Policy, []byte("${aws:username}"), []byte(accountInfo.AccountName))
rawPolicy = bytes.ReplaceAll(rawPolicy, []byte("${aws:userid}"), []byte(accountInfo.AccountName))
// JWT Variables
rawPolicy = replaceJwtVariables(rawPolicy, claims)
// LDAP Variables
rawPolicy = replaceLDAPVariables(rawPolicy, claims)
return rawPolicy
}
func replaceJwtVariables(rawPolicy []byte, claims map[string]interface{}) json.RawMessage {
// list of valid JWT fields we will replace in policy if they are in the response
jwtFields := []string{
"sub",
"iss",
"aud",
"jti",
"upn",
"name",
"groups",
"given_name",
"family_name",
"middle_name",
"nickname",
"preferred_username",
"profile",
"picture",
"website",
"email",
"gender",
"birthdate",
"phone_number",
"address",
"scope",
"client_id",
}
// check which fields are in the claims and replace as variable by casting the value to string
for _, field := range jwtFields {
if val, ok := claims[field]; ok {
variable := fmt.Sprintf("${jwt:%s}", field)
fmt.Println("found", variable)
rawPolicy = bytes.ReplaceAll(rawPolicy, []byte(variable), []byte(fmt.Sprintf("%v", val)))
}
}
return rawPolicy
}
// ReplacePolicyVariables replaces known variables from policies with known values
func replaceLDAPVariables(rawPolicy []byte, claims map[string]interface{}) json.RawMessage {
// replace ${ldap:user}
if val, ok := claims["ldapUser"]; ok {
rawPolicy = bytes.ReplaceAll(rawPolicy, []byte("${ldap:user}"), []byte(fmt.Sprintf("%v", val)))
}
// replace ${ldap:username}
if val, ok := claims["ldapUsername"]; ok {
rawPolicy = bytes.ReplaceAll(rawPolicy, []byte("${ldap:username}"), []byte(fmt.Sprintf("%v", val)))
}
return rawPolicy
}

View File

@@ -0,0 +1,112 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package policy
import (
"bytes"
"reflect"
"testing"
"github.com/minio/madmin-go"
minioIAMPolicy "github.com/minio/pkg/iam/policy"
)
func TestReplacePolicyVariables(t *testing.T) {
type args struct {
claims map[string]interface{}
accountInfo *madmin.AccountInfo
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
name: "Bad Policy",
args: args{
claims: nil,
accountInfo: &madmin.AccountInfo{
AccountName: "test",
Server: madmin.BackendInfo{},
Policy: []byte(""),
Buckets: nil,
},
},
want: "",
wantErr: true,
},
{
name: "Replace basic AWS",
args: args{
claims: nil,
accountInfo: &madmin.AccountInfo{
AccountName: "test",
Server: madmin.BackendInfo{},
Policy: []byte(`{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::${aws:username}",
"arn:aws:s3:::${aws:userid}"
]
}
]
}`),
Buckets: nil,
},
},
want: `{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::test",
"arn:aws:s3:::test"
]
}
]
}`,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ReplacePolicyVariables(tt.args.claims, tt.args.accountInfo)
policy, err := minioIAMPolicy.ParseConfig(bytes.NewReader(got))
if (err != nil) != tt.wantErr {
t.Errorf("ReplacePolicyVariables() error = %v, wantErr %v", err, tt.wantErr)
}
wantPolicy, err := minioIAMPolicy.ParseConfig(bytes.NewReader([]byte(tt.want)))
if (err != nil) != tt.wantErr {
t.Errorf("ReplacePolicyVariables() error = %v, wantErr %v", err, tt.wantErr)
}
if !reflect.DeepEqual(policy, wantPolicy) {
t.Errorf("ReplacePolicyVariables() = %s, want %v", got, tt.want)
}
})
}
}

View File

@@ -17,13 +17,12 @@
package restapi
import (
"bytes"
"context"
"net/http"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/minio/madmin-go"
iampolicy "github.com/minio/pkg/iam/policy"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/middleware"
@@ -88,15 +87,13 @@ func login(credentials ConsoleCredentialsI, sessionFeatures *auth.SessionFeature
return &token, nil
}
// getAccountPolicy will return the associated policy of the current account
func getAccountPolicy(ctx context.Context, client MinioAdmin) (*iampolicy.Policy, error) {
// Obtain the current policy assigned to this user
// necessary for generating the list of allowed endpoints
// getAccountInfo will return the current user information
func getAccountInfo(ctx context.Context, client MinioAdmin) (*madmin.AccountInfo, error) {
accountInfo, err := client.AccountInfo(ctx)
if err != nil {
return nil, err
}
return iampolicy.ParseConfig(bytes.NewReader(accountInfo.Policy))
return &accountInfo, nil
}
// getConsoleCredentials will return ConsoleCredentials interface

View File

@@ -146,7 +146,7 @@ func Test_validateUserAgainstIDP(t *testing.T) {
}
}
func Test_getAccountPolicy(t *testing.T) {
func Test_getAccountInfo(t *testing.T) {
client := adminClientMock{}
type args struct {
ctx context.Context
@@ -160,7 +160,7 @@ func Test_getAccountPolicy(t *testing.T) {
mockFunc func()
}{
{
name: "error getting account policy",
name: "error getting account info",
args: args{
ctx: context.Background(),
client: client,
@@ -179,13 +179,15 @@ func Test_getAccountPolicy(t *testing.T) {
if tt.mockFunc != nil {
tt.mockFunc()
}
got, err := getAccountPolicy(tt.args.ctx, tt.args.client)
got, err := getAccountInfo(tt.args.ctx, tt.args.client)
if (err != nil) != tt.wantErr {
t.Errorf("getAccountPolicy() error = %v, wantErr %v", err, tt.wantErr)
t.Errorf("getAccountInfo() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("getAccountPolicy() got = %v, want %v", got, tt.want)
if tt.want != nil {
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("getAccountInfo() got = %v, want %v", got, tt.want)
}
}
})
}

View File

@@ -17,6 +17,7 @@
package restapi
import (
"bytes"
"context"
"encoding/json"
"net/http"
@@ -24,6 +25,8 @@ import (
"strconv"
"time"
policies "github.com/minio/console/restapi/policy"
jwtgo "github.com/golang-jwt/jwt/v4"
"github.com/minio/pkg/bucket/policy/condition"
@@ -95,6 +98,7 @@ func getSessionResponse(session *models.Principal) (*models.SessionResponse, *mo
if session == nil {
return nil, prepareError(errorGenericInvalidSession)
}
tokenClaims, _ := getClaimsFromToken(session.STSSessionToken)
// initialize admin client
mAdminClient, err := NewMinioAdminClient(&models.Principal{
@@ -108,7 +112,13 @@ func getSessionResponse(session *models.Principal) (*models.SessionResponse, *mo
userAdminClient := AdminClient{Client: mAdminClient}
// Obtain the current policy assigned to this user
// necessary for generating the list of allowed endpoints
policy, err := getAccountPolicy(ctx, userAdminClient)
accountInfo, err := getAccountInfo(ctx, userAdminClient)
if err != nil {
return nil, prepareError(err, errorGenericInvalidSession)
}
rawPolicy := policies.ReplacePolicyVariables(tokenClaims, accountInfo)
policy, err := minioIAMPolicy.ParseConfig(bytes.NewReader(rawPolicy))
if err != nil {
return nil, prepareError(err, errorGenericInvalidSession)
}
@@ -210,12 +220,12 @@ func getSessionResponse(session *models.Principal) (*models.SessionResponse, *mo
resourcePermissions[key] = resourceActions
}
rawPolicy, err := json.Marshal(policy)
serializedPolicy, err := json.Marshal(policy)
if err != nil {
return nil, prepareError(err, errorGenericInvalidSession)
}
var sessionPolicy *models.IamPolicy
err = json.Unmarshal(rawPolicy, &sessionPolicy)
err = json.Unmarshal(serializedPolicy, &sessionPolicy)
if err != nil {
return nil, prepareError(err)
}

View File

@@ -128,6 +128,9 @@ func serveWS(w http.ResponseWriter, req *http.Request) {
errors.ServeError(w, req, errors.New(http.StatusUnauthorized, err.Error()))
return
}
upgrader.CheckOrigin = func(r *http.Request) bool {
return true
}
// upgrades the HTTP server connection to the WebSocket protocol.
conn, err := upgrader.Upgrade(w, req, nil)