diff --git a/integration/login_test.go b/integration/login_test.go index 5e570e9a5..5662a2eeb 100644 --- a/integration/login_test.go +++ b/integration/login_test.go @@ -17,7 +17,9 @@ package integration import ( + "bytes" "encoding/json" + "fmt" "io/ioutil" "log" "net/http" @@ -70,3 +72,66 @@ func TestLoginStrategy(t *testing.T) { } } + +func TestLogout(t *testing.T) { + + assert := assert.New(t) + + // image for now: + // minio: 9000 + // console: 9090 + + client := &http.Client{ + Timeout: 2 * time.Second, + } + requestData := map[string]string{ + "accessKey": "minioadmin", + "secretKey": "minioadmin", + } + + requestDataJSON, _ := json.Marshal(requestData) + + requestDataBody := bytes.NewReader(requestDataJSON) + + request, err := http.NewRequest("POST", "http://localhost:9090/api/v1/login", requestDataBody) + if err != nil { + log.Println(err) + return + } + + request.Header.Add("Content-Type", "application/json") + + response, err := client.Do(request) + + assert.NotNil(response, "Login response is nil") + assert.Nil(err, "Login errored out") + + var loginToken string + + for _, cookie := range response.Cookies() { + if cookie.Name == "token" { + loginToken = cookie.Value + break + } + } + + if loginToken == "" { + log.Println("authentication token not found in cookies response") + return + } + + request, err = http.NewRequest("POST", "http://localhost:9090/api/v1/logout", requestDataBody) + if err != nil { + log.Println(err) + return + } + request.Header.Add("Cookie", fmt.Sprintf("token=%s", loginToken)) + request.Header.Add("Content-Type", "application/json") + + response, err = client.Do(request) + + assert.NotNil(response, "Logout response is nil") + assert.Nil(err, "Logout errored out") + assert.Equal(response.StatusCode, 200) + +} diff --git a/integration/tiers_test.go b/integration/tiers_test.go new file mode 100644 index 000000000..4b70b6343 --- /dev/null +++ b/integration/tiers_test.go @@ -0,0 +1,55 @@ +// 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 . + +package integration + +import ( + "fmt" + "log" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestTiersList(t *testing.T) { + + assert := assert.New(t) + + // image for now: + // minio: 9000 + // console: 9090 + + client := &http.Client{ + Timeout: 2 * time.Second, + } + + request, err := http.NewRequest("GET", "http://localhost:9090/api/v1/admin/tiers", nil) + if err != nil { + log.Println(err) + return + } + request.Header.Add("Cookie", fmt.Sprintf("token=%s", token)) + request.Header.Add("Content-Type", "application/json") + + response, err := client.Do(request) + + assert.NotNil(response, "Tiers List response is nil") + assert.Nil(err, "Tiers List errored out") + assert.Equal(response.StatusCode, 200) + +} diff --git a/restapi/policy/policies.go b/restapi/policy/policies.go new file mode 100644 index 000000000..bc42e5aaa --- /dev/null +++ b/restapi/policy/policies.go @@ -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 . + +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 +} diff --git a/restapi/policy/policies_test.go b/restapi/policy/policies_test.go new file mode 100644 index 000000000..d60fcd59c --- /dev/null +++ b/restapi/policy/policies_test.go @@ -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 . + +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) + } + }) + } +} diff --git a/restapi/user_login.go b/restapi/user_login.go index 7fbc0cc1c..4e1d1e04a 100644 --- a/restapi/user_login.go +++ b/restapi/user_login.go @@ -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 diff --git a/restapi/user_login_test.go b/restapi/user_login_test.go index 06a6ea63f..50a1d1439 100644 --- a/restapi/user_login_test.go +++ b/restapi/user_login_test.go @@ -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) + } } }) } diff --git a/restapi/user_session.go b/restapi/user_session.go index 7ba281368..0855c0047 100644 --- a/restapi/user_session.go +++ b/restapi/user_session.go @@ -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) } diff --git a/restapi/ws_handle.go b/restapi/ws_handle.go index b6fd5a62a..338b1257e 100644 --- a/restapi/ws_handle.go +++ b/restapi/ws_handle.go @@ -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)