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)