Compare commits

..

10 Commits

Author SHA1 Message Date
Daniel Valdivia
35c3b53a23 Dashboard Loading + Prettier files (#124) 2020-05-15 14:41:27 -07:00
César Nieto
6fef30f29d Add Watch api and UI integration (#120)
Uses a similar approach as Trace and Console Logs by using
websockets. It also includes the integration with the UI which
needs 3 input fields that are sent as query parameters.
2020-05-15 14:24:29 -07:00
Alex
acf480fd25 Changed help icon position next to label (#119) 2020-05-12 19:54:24 -05:00
César Nieto
8bbc4f0192 remove color dependency and any non ascii characters (#118) 2020-05-12 17:20:58 -07:00
Alex
48e7991f11 Fixed label size & added text alignment (#112)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-05-12 14:34:48 -07:00
Alex
5e9b0652b0 Changed modal forms to contain scrollbars (#113)
Changed modal forms to contain scrollbars where the inputs are, so this way the titles and buttons get always visible.

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
Co-authored-by: Daniel Valdivia <hola@danielvaldivia.com>
2020-05-12 13:41:09 -05:00
Lenin Alevski
438211199d LDAP authentication support for MCS (#114)
This PR adds ldap authentication support for mcs based on
https://github.com/minio/minio/blob/master/docs/sts/ldap.md

How to test:

```
$ docker run --rm -p 389:389 -p 636:636 --name my-openldap-container
--detach osixia/openldap:1.3.0
```

Run the `billy.ldif` file using `ldapadd` command to create a new user
and assign it to a group.

```
$ cat > billy.ldif << EOF
dn: uid=billy,dc=example,dc=org
uid: billy
cn: billy
sn: 3
objectClass: top
objectClass: posixAccount
objectClass: inetOrgPerson
loginShell: /bin/bash
homeDirectory: /home/billy
uidNumber: 14583102
gidNumber: 14564100
userPassword: {SSHA}j3lBh1Seqe4rqF1+NuWmjhvtAni1JC5A
mail: billy@example.org
gecos: Billy User
dn: ou=groups,dc=example,dc=org
objectclass:organizationalunit
ou: groups
description: generic groups branch
of s3::*)
dn: cn=mcsAdmin,ou=groups,dc=example,dc=org
objectClass: top
objectClass: posixGroup
gidNumber: 678
dn: cn=mcsAdmin,ou=groups,dc=example,dc=org
changetype: modify
add: memberuid
memberuid: billy
EOF

$ docker cp billy.ldif
my-openldap-container:/container/service/slapd/assets/test/billy.ldif
$ docker exec my-openldap-container ldapadd -x -D
"cn=admin,dc=example,dc=org" -w admin -f
/container/service/slapd/assets/test/billy.ldif -H ldap://localhost -ZZ
```

Query the ldap server to check the user billy was created correctly and
got assigned to the mcsAdmin group, you should get a list
containing ldap users and groups.

```
$ docker exec my-openldap-container ldapsearch -x -H ldap://localhost -b
dc=example,dc=org -D "cn=admin,dc=example,dc=org" -w admin
```

Query the ldap server again, this time filtering only for the user
`billy`, you should see only 1 record.

```
$ docker exec my-openldap-container ldapsearch -x -H ldap://localhost -b
uid=billy,dc=example,dc=org -D "cn=admin,dc=example,dc=org" -w admin
```

Change the password for user billy

Set the new password for `billy` to `minio123` and enter `admin` as the
default `LDAP Password`

```
$ docker exec -it my-openldap-container /bin/bash
ldappasswd -H ldap://localhost -x -D "cn=admin,dc=example,dc=org" -W
-S "uid=billy,dc=example,dc=org"
New password:
Re-enter new password:
Enter LDAP Password:
```

Add the mcsAdmin policy to user billy on MinIO

```
$ cat > mcsAdmin.json << EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": [
        "admin:*"
      ],
      "Effect": "Allow",
      "Sid": ""
    },
    {
      "Action": [
        "s3:*"
      ],
      "Effect": "Allow",
      "Resource": [
        "arn:aws:s3:::*"
      ],
      "Sid": ""
    }
  ]
}
EOF
$ mc admin policy add myminio mcsAdmin mcsAdmin.json
$ mc admin policy set myminio mcsAdmin user=billy
```

Run MinIO

```
export MINIO_ACCESS_KEY=minio
export MINIO_SECRET_KEY=minio123
export MINIO_IDENTITY_LDAP_SERVER_ADDR='localhost:389'
export MINIO_IDENTITY_LDAP_USERNAME_FORMAT='uid=%s,dc=example,dc=org'
export
MINIO_IDENTITY_LDAP_USERNAME_SEARCH_FILTER='(|(objectclass=posixAccount)(uid=%s))'
export MINIO_IDENTITY_LDAP_TLS_SKIP_VERIFY=on
export MINIO_IDENTITY_LDAP_SERVER_INSECURE=on
./minio server ~/Data
```

Run MCS

```
export MCS_ACCESS_KEY=minio
export MCS_SECRET_KEY=minio123
...
export MCS_LDAP_ENABLED=on
./mcs server
```
2020-05-12 10:26:38 -07:00
Alex
0f77a32656 Fixed delete bucket event functionality (#109)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-05-09 10:16:15 -07:00
Daniel Valdivia
9f3e99ede8 Fix UI Metadata (#108) 2020-05-08 19:38:51 -07:00
Lenin Alevski
a8c07c0969 Connect MCS with Minio insecure TLS/Custom CAs (#102)
This PR adds support to connect MCS to minio instances running TLS with
self-signed certificates or  certificates signed by custom
Certificate Authorities

```
export MCS_MINIO_SERVER_TLS_ROOT_CAS=file1,file2,file3
```

Note: TLS Skip Verification is not supported unless there's a clear need
for it
2020-05-08 17:11:47 -07:00
56 changed files with 2426 additions and 1046 deletions

127
DEVELOPMENT.md Normal file
View File

@@ -0,0 +1,127 @@
# LDAP authentication with MCS
## Setup
Run openLDAP with docker.
```
$ docker run --rm -p 389:389 -p 636:636 --name my-openldap-container --detach osixia/openldap:1.3.0
```
Run the `billy.ldif` file using `ldapadd` command to create a new user and assign it to a group.
```
$ cat > billy.ldif << EOF
# LDIF fragment to create group branch under root
dn: uid=billy,dc=example,dc=org
uid: billy
cn: billy
sn: 3
objectClass: top
objectClass: posixAccount
objectClass: inetOrgPerson
loginShell: /bin/bash
homeDirectory: /home/billy
uidNumber: 14583102
gidNumber: 14564100
userPassword: {SSHA}j3lBh1Seqe4rqF1+NuWmjhvtAni1JC5A
mail: billy@example.org
gecos: Billy User
# Create base group
dn: ou=groups,dc=example,dc=org
objectclass:organizationalunit
ou: groups
description: generic groups branch
# create mcsAdmin group (this already exists on minio and have a policy of s3::*)
dn: cn=mcsAdmin,ou=groups,dc=example,dc=org
objectClass: top
objectClass: posixGroup
gidNumber: 678
# Assing group to new user
dn: cn=mcsAdmin,ou=groups,dc=example,dc=org
changetype: modify
add: memberuid
memberuid: billy
EOF
$ docker cp billy.ldif my-openldap-container:/container/service/slapd/assets/test/billy.ldif
$ docker exec my-openldap-container ldapadd -x -D "cn=admin,dc=example,dc=org" -w admin -f /container/service/slapd/assets/test/billy.ldif -H ldap://localhost -ZZ
```
Query the ldap server to check the user billy was created correctly and got assigned to the mcsAdmin group, you should get a list
containing ldap users and groups.
```
$ docker exec my-openldap-container ldapsearch -x -H ldap://localhost -b dc=example,dc=org -D "cn=admin,dc=example,dc=org" -w admin
```
Query the ldap server again, this time filtering only for the user `billy`, you should see only 1 record.
```
$ docker exec my-openldap-container ldapsearch -x -H ldap://localhost -b uid=billy,dc=example,dc=org -D "cn=admin,dc=example,dc=org" -w admin
```
### Change the password for user billy
Set the new password for `billy` to `minio123` and enter `admin` as the default `LDAP Password`
```
$ docker exec -it my-openldap-container /bin/bash
# ldappasswd -H ldap://localhost -x -D "cn=admin,dc=example,dc=org" -W -S "uid=billy,dc=example,dc=org"
New password:
Re-enter new password:
Enter LDAP Password:
```
### Add the mcsAdmin policy to user billy on MinIO
```
$ cat > mcsAdmin.json << EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"admin:*"
],
"Effect": "Allow",
"Sid": ""
},
{
"Action": [
"s3:*"
],
"Effect": "Allow",
"Resource": [
"arn:aws:s3:::*"
],
"Sid": ""
}
]
}
EOF
$ mc admin policy add myminio mcsAdmin mcsAdmin.json
$ mc admin policy set myminio mcsAdmin user=billy
```
## Run MinIO
```
export MINIO_ACCESS_KEY=minio
export MINIO_SECRET_KEY=minio123
export MINIO_IDENTITY_LDAP_SERVER_ADDR='localhost:389'
export MINIO_IDENTITY_LDAP_USERNAME_FORMAT='uid=%s,dc=example,dc=org'
export MINIO_IDENTITY_LDAP_USERNAME_SEARCH_FILTER='(|(objectclass=posixAccount)(uid=%s))'
export MINIO_IDENTITY_LDAP_TLS_SKIP_VERIFY=on
export MINIO_IDENTITY_LDAP_SERVER_INSECURE=on
./minio server ~/Data
```
## Run MCS
```
export MCS_ACCESS_KEY=minio
export MCS_SECRET_KEY=minio123
...
export MCS_LDAP_ENABLED=on
./mcs server
```

View File

@@ -68,6 +68,15 @@ export MCS_MINIO_SERVER=http://localhost:9000
./mcs server
```
## Connect MCS to a Minio using TLS and a self-signed certificate
```
...
export MCS_MINIO_SERVER_TLS_SKIP_VERIFICATION=on
export MCS_MINIO_SERVER=https://localhost:9000
./mcs server
```
You can verify that the apis work by doing the request on `localhost:9090/api/v1/...`
# Contribute to mcs Project

4
go.mod
View File

@@ -17,9 +17,9 @@ require (
github.com/jessevdk/go-flags v1.4.0
github.com/json-iterator/go v1.1.9
github.com/minio/cli v1.22.0
github.com/minio/mc v0.0.0-20200415193718-68b638f2f96c
github.com/minio/mc v0.0.0-20200515191050-09c31c4ab28c
github.com/minio/minio v0.0.0-20200501193630-d1c8e9f31ba0
github.com/minio/minio-go/v6 v6.0.55-0.20200424204115-7506d2996b22
github.com/minio/minio-go/v6 v6.0.56-0.20200502013257-a81c8c13cc3f
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/satori/go.uuid v1.2.0
github.com/stretchr/testify v1.5.1

12
go.sum
View File

@@ -12,6 +12,7 @@ github.com/Azure/azure-storage-blob-go v0.8.0 h1:53qhf0Oxa0nOjgbDeeYPUeyiNmafAFE
github.com/Azure/azure-storage-blob-go v0.8.0/go.mod h1:lPI3aLPpuLTeUwh1sViKXFxwl2B6teiRqI0deQUvsw0=
github.com/Azure/go-autorest v11.7.1+incompatible h1:M2YZIajBBVekV86x0rr1443Lc1F/Ylxb9w+5EtSyX3Q=
github.com/Azure/go-autorest v11.7.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
@@ -30,7 +31,6 @@ github.com/alecthomas/participle v0.2.1 h1:4AVLj1viSGa4LG5HDXKXrm5xRx19SB/rS/skP
github.com/alecthomas/participle v0.2.1/go.mod h1:SW6HZGeZgSIpcUWX3fXpfZhuaWHnmoD5KCVaqSaNTkk=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190307165228-86c17b95fcd5/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878 h1:EFSB7Zo9Eg91v7MJPVsifUysc/wPdN+NOnVe6bWbdBM=
@@ -41,7 +41,6 @@ github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:l
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 h1:zV3ejI06GQ59hwDQAvmK1qxOQGB3WuVTRoY0okPTAv0=
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
github.com/aws/aws-sdk-go v1.20.21/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc=
github.com/bcicen/jstream v0.0.0-20190220045926-16c1f8af81c2 h1:M+TYzBcNIRyzPRg66ndEqUMd7oWDmhvdQmaPC6EZNwM=
github.com/bcicen/jstream v0.0.0-20190220045926-16c1f8af81c2/go.mod h1:RDu/qcrnpEdJC/p8tx34+YBFqqX71lB7dOX9QE+ZC4M=
github.com/beevik/ntp v0.2.0 h1:sGsd+kAXzT0bfVfzJfce04g+dSRfrs+tbQW8lweuYgw=
@@ -114,6 +113,7 @@ github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32/go.mod h1:GIjDIg/heH
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-ldap/ldap v3.0.2+incompatible h1:kD5HQcAzlQ7yrhfn+h+MSABeAy/jAJhvIJ/QDllP44g=
github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
@@ -399,15 +399,17 @@ github.com/minio/highwayhash v1.0.0 h1:iMSDhgUILCr0TNm8LWlSjF8N0ZIj2qbO8WHp6Q/J2
github.com/minio/highwayhash v1.0.0/go.mod h1:xQboMTeM9nY9v/LlAOxFctujiv5+Aq2hR5dxBpaMbdc=
github.com/minio/lsync v1.0.1 h1:AVvILxA976xc27hstd1oR+X9PQG0sPSom1MNb1ImfUs=
github.com/minio/lsync v1.0.1/go.mod h1:tCFzfo0dlvdGl70IT4IAK/5Wtgb0/BrTmo/jE8pArKA=
github.com/minio/mc v0.0.0-20200415193718-68b638f2f96c h1:JLr0fYpCleodj9nGB5hfsJU2zPdnNQKqa2bYsIvPhVw=
github.com/minio/mc v0.0.0-20200415193718-68b638f2f96c/go.mod h1:l9PuOY62zT7AQJqopDjfo/T22AIBJSb2yXPVZf4RlhM=
github.com/minio/minio v0.0.0-20200415191640-bde0f444dbab/go.mod h1:v8oQPMMaTkjDwp5cOz1WCElA4Ik+X+0y4On+VMk0fis=
github.com/minio/mc v0.0.0-20200515191050-09c31c4ab28c h1:G4ZTNwiiJ73folxqNXZpWQofxus2fGJG7hKxYNrtvRM=
github.com/minio/mc v0.0.0-20200515191050-09c31c4ab28c/go.mod h1:U3Jgk0bcSjn+QPUMisrS6nxCWOoQ6rYWSvLCB30apuU=
github.com/minio/minio v0.0.0-20200421050159-282c9f790a03/go.mod h1:zBua5AiljGs1Irdl2XEyiJjvZVCVDIG8gjozzRBcVlw=
github.com/minio/minio v0.0.0-20200501193630-d1c8e9f31ba0 h1:QxIz36O01LbKqJiz6HKeKCOC2afgydspkpahQ807msY=
github.com/minio/minio v0.0.0-20200501193630-d1c8e9f31ba0/go.mod h1:Vhlqz7Se0EgpgFiVxpvzF4Zz/h2LMx+EPKH96Aera5U=
github.com/minio/minio-go/v6 v6.0.53 h1:8jzpwiOzZ5Iz7/goFWqNZRICbyWYShbb5rARjrnSCNI=
github.com/minio/minio-go/v6 v6.0.53/go.mod h1:DIvC/IApeHX8q1BAMVCXSXwpmrmM+I+iBvhvztQorfI=
github.com/minio/minio-go/v6 v6.0.55-0.20200424204115-7506d2996b22 h1:nZEve4vdUhwHBoV18zRvPDgjL6NYyDJE5QJvz3l9bRs=
github.com/minio/minio-go/v6 v6.0.55-0.20200424204115-7506d2996b22/go.mod h1:KQMM+/44DSlSGSQWSfRrAZ12FVMmpWNuX37i2AX0jfI=
github.com/minio/minio-go/v6 v6.0.56-0.20200502013257-a81c8c13cc3f h1:ifHrI8+exqLi5RztIWWKS5k+Wu+W7DJisVXwNaCH2zs=
github.com/minio/minio-go/v6 v6.0.56-0.20200502013257-a81c8c13cc3f/go.mod h1:KQMM+/44DSlSGSQWSfRrAZ12FVMmpWNuX37i2AX0jfI=
github.com/minio/parquet-go v0.0.0-20200414234858-838cfa8aae61 h1:pUSI/WKPdd77gcuoJkSzhJ4wdS8OMDOsOu99MtpXEQA=
github.com/minio/parquet-go v0.0.0-20200414234858-838cfa8aae61/go.mod h1:4trzEJ7N1nBTd5Tt7OCZT5SEin+WiAXpdJ/WgPkESA8=
github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU=

39
pkg/auth/ldap.go Normal file
View File

@@ -0,0 +1,39 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 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 auth
import (
"errors"
"log"
"github.com/minio/minio-go/v6/pkg/credentials"
)
var (
errInvalidCredentials = errors.New("invalid Credentials")
)
// GetMcsCredentialsFromLDAP authenticates the user against MinIO when the LDAP integration is enabled
// if the authentication succeed *credentials.Credentials object is returned and we continue with the normal STSAssumeRole flow
func GetMcsCredentialsFromLDAP(endpoint, ldapUser, ldapPassword string) (*credentials.Credentials, error) {
creds, err := credentials.NewLDAPIdentity(endpoint, ldapUser, ldapPassword)
if err != nil {
log.Println("LDAP authentication error: ", err)
return nil, errInvalidCredentials
}
return creds, nil
}

27
pkg/auth/ldap/config.go Normal file
View File

@@ -0,0 +1,27 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 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 ldap
import (
"strings"
"github.com/minio/minio/pkg/env"
)
func GetLDAPEnabled() bool {
return strings.ToLower(env.Get(MCSLDAPEnabled, "off")) == "on"
}

22
pkg/auth/ldap/const.go Normal file
View File

@@ -0,0 +1,22 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 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 ldap
const (
// const for ldap configuration
MCSLDAPEnabled = "MCS_LDAP_ENABLED"
)

View File

@@ -22,13 +22,14 @@ import (
"strings"
"github.com/go-openapi/errors"
"github.com/minio/mcs/pkg/auth"
"github.com/go-openapi/swag"
)
// Authenticate validates websocket header and returns mcs jwt claims
// GetTokenFromRequest returns a token from a http Request
// either defined on a cookie `token` or on Authorization header.
//
// Authorization Header needs to be like "Authorization Bearer <jwt_token>"
func Authenticate(r *http.Request) (*auth.DecryptedClaims, error) {
func GetTokenFromRequest(r *http.Request) (*string, error) {
// Get Auth token
var reqToken string
@@ -46,11 +47,5 @@ func Authenticate(r *http.Request) (*auth.DecryptedClaims, error) {
} else {
reqToken = strings.TrimSpace(tokenCookie.Value)
}
// Perform authentication before upgrading to a Websocket Connection
claims, err := auth.JWTAuthenticate(reqToken)
if err != nil {
return nil, errors.New(http.StatusUnauthorized, err.Error())
}
return claims, nil
return swag.String(reqToken), nil
}

File diff suppressed because one or more lines are too long

View File

@@ -6,7 +6,7 @@
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Acme cloud storage"
content="MinIO Console"
/>
<!--
manifest.json provides metadata used when your web app is installed on a

View File

@@ -38,3 +38,12 @@ export const setCookie = (name: string, val: string) => {
document.cookie =
name + "=" + value + "; expires=" + date.toUTCString() + "; path=/";
};
// timeFromdate gets time string from date input
export const timeFromDate = (d: Date) => {
let h = d.getHours() < 10 ? `0${d.getHours()}` : `${d.getHours()}`;
let m = d.getMinutes() < 10 ? `0${d.getMinutes()}` : `${d.getMinutes()}`;
let s = d.getSeconds() < 10 ? `0${d.getSeconds()}` : `${d.getSeconds()}`;
return `${h}:${m}:${s}:${d.getMilliseconds()}`;
};

View File

@@ -19,6 +19,7 @@ import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import { Button, LinearProgress } from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
import api from "../../../../common/api";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
@@ -26,11 +27,12 @@ import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBo
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
color: "red",
},
buttonContainer: {
textAlign: "right"
}
textAlign: "right",
},
...modalBasic,
});
interface IAddBucketProps {
@@ -49,7 +51,7 @@ class AddBucket extends React.Component<IAddBucketProps, IAddBucketState> {
state: IAddBucketState = {
addLoading: false,
addError: "",
bucketName: ""
bucketName: "",
};
addRecord(event: React.FormEvent) {
@@ -61,23 +63,23 @@ class AddBucket extends React.Component<IAddBucketProps, IAddBucketState> {
this.setState({ addLoading: true }, () => {
api
.invoke("POST", "/api/v1/buckets", {
name: bucketName
name: bucketName,
})
.then(res => {
.then((res) => {
this.setState(
{
addLoading: false,
addError: ""
addError: "",
},
() => {
this.props.closeModalAndRefresh();
}
);
})
.catch(err => {
.catch((err) => {
this.setState({
addLoading: false,
addError: err
addError: err,
});
});
});
@@ -106,31 +108,29 @@ class AddBucket extends React.Component<IAddBucketProps, IAddBucketState> {
}}
>
<Grid container>
{addError !== "" && (
<Grid item xs={12} className={classes.formScrollable}>
{addError !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
</Grid>
)}
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
<InputBoxWrapper
id="bucket-name"
name="bucket-name"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ bucketName: e.target.value });
}}
label="Bucket Name"
value={bucketName}
/>
</Grid>
)}
<Grid item xs={12}>
<InputBoxWrapper
id="bucket-name"
name="bucket-name"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ bucketName: e.target.value });
}}
label="Bucket Name"
value={bucketName}
/>
</Grid>
<Grid item xs={12}>
<br />
<br />
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>
<Button

View File

@@ -27,6 +27,7 @@ import TableBody from "@material-ui/core/TableBody";
import Checkbox from "@material-ui/core/Checkbox";
import Table from "@material-ui/core/Table";
import { ArnList } from "../types";
import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
@@ -34,19 +35,20 @@ import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapp
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
color: "red",
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold"
}
}
fontWeight: "bold",
},
},
},
buttonContainer: {
textAlign: "right"
}
textAlign: "right",
},
...modalBasic,
});
interface IAddEventProps {
@@ -74,7 +76,7 @@ class AddEvent extends React.Component<IAddEventProps, IAddEventState> {
suffix: "",
arn: "",
selectedEvents: [],
arnList: []
arnList: [],
};
addRecord(event: React.FormEvent) {
@@ -91,25 +93,25 @@ class AddEvent extends React.Component<IAddEventProps, IAddEventState> {
arn: arn,
events: selectedEvents,
prefix: prefix,
suffix: suffix
suffix: suffix,
},
ignoreExisting: true
ignoreExisting: true,
})
.then(res => {
.then((res) => {
this.setState(
{
addLoading: false,
addError: ""
addError: "",
},
() => {
this.props.closeModalAndRefresh();
}
);
})
.catch(err => {
.catch((err) => {
this.setState({
addLoading: false,
addError: err
addError: err,
});
});
});
@@ -127,7 +129,7 @@ class AddEvent extends React.Component<IAddEventProps, IAddEventState> {
this.setState({
addLoading: false,
arnList: arns,
addError: ""
addError: "",
});
})
.catch((err: any) => {
@@ -149,13 +151,13 @@ class AddEvent extends React.Component<IAddEventProps, IAddEventState> {
selectedEvents,
arnList,
prefix,
suffix
suffix,
} = this.state;
const events = [
{ label: "PUT - Object Uploaded", value: "put" },
{ label: "GET - Object accessed", value: "get" },
{ label: "DELETE - Object Deleted", value: "delete" }
{ label: "DELETE - Object Deleted", value: "delete" },
];
const handleClick = (
@@ -181,9 +183,9 @@ class AddEvent extends React.Component<IAddEventProps, IAddEventState> {
this.setState({ selectedEvents: newSelected });
};
const arnValues = arnList.map(arnConstant => ({
const arnValues = arnList.map((arnConstant) => ({
label: arnConstant,
value: arnConstant
value: arnConstant,
}));
return (
@@ -204,89 +206,91 @@ class AddEvent extends React.Component<IAddEventProps, IAddEventState> {
}}
>
<Grid container>
{addError !== "" && (
<Grid item xs={12} className={classes.formScrollable}>
{addError !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
</Grid>
)}
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
<SelectWrapper
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
this.setState({ arn: e.target.value as string });
}}
id="select-access-policy"
name="select-access-policy"
label={"ARN"}
value={arn}
options={arnValues}
/>
</Grid>
)}
<Grid item xs={12}>
<SelectWrapper
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
this.setState({ arn: e.target.value as string });
}}
id="select-access-policy"
name="select-access-policy"
label={"ARN"}
value={arn}
options={arnValues}
/>
</Grid>
<Grid item xs={12}>
<Table size="medium">
<TableHead className={classes.minTableHeader}>
<TableRow>
<TableCell>Select</TableCell>
<TableCell>Event</TableCell>
</TableRow>
</TableHead>
<TableBody>
{events.map(row => (
<TableRow
key={`group-${row.value}`}
onClick={event => handleClick(event, row.value)}
>
<TableCell padding="checkbox">
<Checkbox
value={row.value}
color="primary"
inputProps={{
"aria-label": "secondary checkbox"
}}
onChange={event => handleClick(event, row.value)}
checked={selectedEvents.includes(row.value)}
/>
</TableCell>
<TableCell className={classes.wrapCell}>
{row.label}
</TableCell>
<Grid item xs={12}>
<Table size="medium">
<TableHead className={classes.minTableHeader}>
<TableRow>
<TableCell>Select</TableCell>
<TableCell>Event</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="prefix-input"
name="prefix-input"
label="Prefix"
value={prefix}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ prefix: e.target.value });
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="suffix-input"
name="suffix-input"
label="Suffix"
value={suffix}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ suffix: e.target.value });
}}
/>
</Grid>
<Grid item xs={12}>
<br />
</TableHead>
<TableBody>
{events.map((row) => (
<TableRow
key={`group-${row.value}`}
onClick={(event) => handleClick(event, row.value)}
>
<TableCell padding="checkbox">
<Checkbox
value={row.value}
color="primary"
inputProps={{
"aria-label": "secondary checkbox",
}}
onChange={(event) => handleClick(event, row.value)}
checked={selectedEvents.includes(row.value)}
/>
</TableCell>
<TableCell className={classes.wrapCell}>
{row.label}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="prefix-input"
name="prefix-input"
label="Prefix"
value={prefix}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ prefix: e.target.value });
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="suffix-input"
name="suffix-input"
label="Suffix"
value={suffix}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ suffix: e.target.value });
}}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>
<Button

View File

@@ -14,8 +14,9 @@
// 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/>.
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import React from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import get from "lodash/get";
import {
Button,
Dialog,
@@ -23,7 +24,7 @@ import {
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress
LinearProgress,
} from "@material-ui/core";
import api from "../../../../common/api";
import { BucketEvent, BucketList } from "../types";
@@ -32,8 +33,8 @@ import Typography from "@material-ui/core/Typography";
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
}
color: "red",
},
});
interface IDeleteEventProps {
@@ -55,7 +56,7 @@ class DeleteEvent extends React.Component<
> {
state: IDeleteEventState = {
deleteLoading: false,
deleteError: ""
deleteError: "",
};
removeRecord() {
@@ -69,29 +70,34 @@ class DeleteEvent extends React.Component<
}
this.setState({ deleteLoading: true }, () => {
const events = get(bucketEvent, "events", []);
const prefix = get(bucketEvent, "prefix", "");
const suffix = get(bucketEvent, "suffix", "");
api
.invoke(
"DELETE",
`/api/v1/buckets/${selectedBucket}/events/${bucketEvent.id}`,
`/api/v1/buckets/${selectedBucket}/events/${bucketEvent.arn}`,
{
name: selectedBucket
events,
prefix,
suffix,
}
)
.then((res: BucketList) => {
this.setState(
{
deleteLoading: false,
deleteError: ""
deleteError: "",
},
() => {
this.props.closeDeleteModalAndRefresh(true);
}
);
})
.catch(err => {
.catch((err) => {
this.setState({
deleteLoading: false,
deleteError: err
deleteError: err,
});
});
});

View File

@@ -18,6 +18,7 @@ import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import { Button, LinearProgress } from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
import api from "../../../../common/api";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
@@ -25,8 +26,9 @@ import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapp
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
}
color: "red",
},
...modalBasic,
});
interface ISetAccessPolicyProps {
@@ -49,7 +51,7 @@ class SetAccessPolicy extends React.Component<
state: ISetAccessPolicyState = {
addLoading: false,
addError: "",
accessPolicy: ""
accessPolicy: "",
};
addRecord(event: React.FormEvent) {
@@ -62,23 +64,23 @@ class SetAccessPolicy extends React.Component<
this.setState({ addLoading: true }, () => {
api
.invoke("PUT", `/api/v1/buckets/${bucketName}/set-policy`, {
access: accessPolicy
access: accessPolicy,
})
.then(res => {
.then((res) => {
this.setState(
{
addLoading: false,
addError: ""
addError: "",
},
() => {
this.props.closeModalAndRefresh();
}
);
})
.catch(err => {
.catch((err) => {
this.setState({
addLoading: false,
addError: err
addError: err,
});
});
});
@@ -105,34 +107,33 @@ class SetAccessPolicy extends React.Component<
}}
>
<Grid container>
{addError !== "" && (
<Grid item xs={12} className={classes.formScrollable}>
{addError !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
</Grid>
)}
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
<SelectWrapper
value={accessPolicy}
label="Access Policy"
id="select-access-policy"
name="select-access-policy"
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
this.setState({ accessPolicy: e.target.value as string });
}}
options={[
{ value: "PRIVATE", label: "Private" },
{ value: "PUBLIC", label: "Public" },
]}
/>
</Grid>
)}
<Grid item xs={12}>
<SelectWrapper
value={accessPolicy}
label="Access Policy"
id="select-access-policy"
name="select-access-policy"
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
this.setState({ accessPolicy: e.target.value as string });
}}
options={[
{ value: "PRIVATE", label: "Private" },
{ value: "PUBLIC", label: "Public" }
]}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<Button

View File

@@ -32,50 +32,50 @@ import TableWrapper from "../../Common/TableWrapper/TableWrapper";
const styles = (theme: Theme) =>
createStyles({
seeMore: {
marginTop: theme.spacing(3)
marginTop: theme.spacing(3),
},
paper: {
display: "flex",
overflow: "auto",
flexDirection: "column"
flexDirection: "column",
},
addSideBar: {
width: "320px",
padding: "20px"
padding: "20px",
},
errorBlock: {
color: "red"
color: "red",
},
tableToolbar: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(0)
paddingRight: theme.spacing(0),
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold"
}
}
fontWeight: "bold",
},
},
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10
}
marginLeft: 10,
},
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012"
boxShadow: "0px 3px 6px #00000012",
},
noRecords: {
lineHeight: "24px",
textAlign: "center",
padding: "20px"
}
padding: "20px",
},
});
interface IViewBucketProps {
@@ -113,7 +113,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
addScreenOpen: false,
deleteOpen: false,
selectedBucket: "",
selectedEvent: null
selectedEvent: null,
};
fetchEvents() {
@@ -131,7 +131,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
loading: false,
records: events || [],
totalRecords: total,
error: ""
error: "",
});
// if we get 0 results, and page > 0 , go down 1 page
if ((!events || res.events.length === 0) && page > 0) {
@@ -169,7 +169,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
.then((res: BucketInfo) => {
this.setState({ info: res });
})
.catch(err => {});
.catch((err) => {});
}
componentDidMount(): void {
@@ -191,7 +191,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
rowsPerPage,
deleteOpen,
addScreenOpen,
selectedEvent
selectedEvent,
} = this.state;
const offset = page * rowsPerPage;
@@ -228,21 +228,26 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
return (
<React.Fragment>
<AddEvent
open={addScreenOpen}
selectedBucket={bucketName}
closeModalAndRefresh={() => {
this.setState({ addScreenOpen: false });
this.fetchEvents();
}}
/>
<SetAccessPolicy
bucketName={bucketName}
open={setAccessPolicyScreenOpen}
closeModalAndRefresh={() => {
this.closeAddModalAndRefresh();
}}
/>
{addScreenOpen && (
<AddEvent
open={addScreenOpen}
selectedBucket={bucketName}
closeModalAndRefresh={() => {
this.setState({ addScreenOpen: false });
this.fetchEvents();
}}
/>
)}
{setAccessPolicyScreenOpen && (
<SetAccessPolicy
bucketName={bucketName}
open={setAccessPolicyScreenOpen}
closeModalAndRefresh={() => {
this.closeAddModalAndRefresh();
}}
/>
)}
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">
@@ -261,7 +266,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
color="primary"
onClick={() => {
this.setState({
setAccessPolicyScreenOpen: true
setAccessPolicyScreenOpen: true,
});
}}
>
@@ -285,7 +290,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
startIcon={<CreateIcon />}
onClick={() => {
this.setState({
addScreenOpen: true
addScreenOpen: true,
});
}}
>
@@ -303,10 +308,10 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
{
label: "Events",
elementKey: "events",
renderFunction: eventsDisplay
renderFunction: eventsDisplay,
},
{ label: "Prefix", elementKey: "prefix" },
{ label: "Suffix", elementKey: "suffix" }
{ label: "Suffix", elementKey: "suffix" },
]}
isLoading={loading}
records={filteredRecords}
@@ -320,11 +325,11 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
page: page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true
native: true,
},
onChangePage: handleChangePage,
onChangeRowsPerPage: handleChangeRowsPerPage,
ActionsComponent: MinTablePaginationActions
ActionsComponent: MinTablePaginationActions,
}}
/>
</Grid>

View File

@@ -19,7 +19,7 @@ import Grid from "@material-ui/core/Grid";
import get from "lodash/get";
import InputBoxWrapper from "../InputBoxWrapper/InputBoxWrapper";
import { InputLabel, Tooltip } from "@material-ui/core";
import { fieldBasic } from "../common/styleLibrary";
import { fieldBasic, tooltipHelper } from "../common/styleLibrary";
import HelpIcon from "@material-ui/icons/Help";
interface ICSVMultiSelector {
@@ -34,9 +34,10 @@ interface ICSVMultiSelector {
const styles = (theme: Theme) =>
createStyles({
...fieldBasic,
...tooltipHelper,
inputLabel: {
...fieldBasic.inputLabel,
marginBottom: 10
width: 116,
},
inputContainer: {
height: 150,
@@ -44,11 +45,10 @@ const styles = (theme: Theme) =>
padding: 15,
position: "relative",
border: "1px solid #c4c4c4",
marginBottom: 10
},
labelContainer: {
display: "flex"
}
display: "flex",
},
});
const CSVMultiSelector = ({
@@ -57,7 +57,7 @@ const CSVMultiSelector = ({
label,
tooltip = "",
onChange,
classes
classes,
}: ICSVMultiSelector) => {
const [currentElements, setCurrentElements] = useState<string[]>([""]);
const bottomList = createRef<HTMLDivElement>();
@@ -80,7 +80,7 @@ const CSVMultiSelector = ({
// Use effect to send new values to onChange
useEffect(() => {
const elementsString = currentElements
.filter(element => element.trim() !== "")
.filter((element) => element.trim() !== "")
.join(",");
onChange(elementsString);
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -129,18 +129,20 @@ const CSVMultiSelector = ({
return (
<React.Fragment>
<Grid item xs={12} className={classes.fieldContainer}>
<InputLabel className={classes.inputLabel}>{label}</InputLabel>
<InputLabel className={classes.inputLabel}>
<span>{label}</span>
{tooltip !== "" && (
<div className={classes.tooltipContainer}>
<Tooltip title={tooltip} placement="top-start">
<HelpIcon className={classes.tooltip} />
</Tooltip>
</div>
)}
</InputLabel>
<Grid item xs={12} className={classes.inputContainer}>
{inputs}
<div ref={bottomList} />
</Grid>
{tooltip !== "" && (
<div className={classes.tooltipContainer}>
<Tooltip title={tooltip} placement="left">
<HelpIcon />
</Tooltip>
</div>
)}
</Grid>
</React.Fragment>
);

View File

@@ -19,16 +19,16 @@ import {
InputLabel,
TextField,
TextFieldProps,
Tooltip
Tooltip,
} from "@material-ui/core";
import { OutlinedInputProps } from "@material-ui/core/OutlinedInput";
import {
createStyles,
makeStyles,
Theme,
withStyles
withStyles,
} from "@material-ui/core/styles";
import { fieldBasic } from "../common/styleLibrary";
import { fieldBasic, tooltipHelper } from "../common/styleLibrary";
import HelpIcon from "@material-ui/icons/Help";
interface InputBoxProps {
@@ -49,22 +49,23 @@ interface InputBoxProps {
const styles = (theme: Theme) =>
createStyles({
...fieldBasic,
...tooltipHelper,
textBoxContainer: {
flexGrow: 1
}
flexGrow: 1,
},
});
const inputStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
borderColor: "#393939",
borderRadius: 0
borderRadius: 0,
},
input: {
padding: "11px 20px",
color: "#393939",
fontSize: 14
}
fontSize: 14,
},
})
);
@@ -91,16 +92,24 @@ const InputBoxWrapper = ({
multiline = false,
tooltip = "",
index = 0,
classes
classes,
}: InputBoxProps) => {
return (
<React.Fragment>
<Grid item xs={12} className={classes.fieldContainer}>
{label !== "" && (
<InputLabel htmlFor={id} className={classes.inputLabel}>
{label}
<span>{label}</span>
{tooltip !== "" && (
<div className={classes.tooltipContainer}>
<Tooltip title={tooltip} placement="top-start">
<HelpIcon className={classes.tooltip} />
</Tooltip>
</div>
)}
</InputLabel>
)}
<div className={classes.textBoxContainer}>
<InputField
className={classes.boxDesign}
@@ -117,13 +126,6 @@ const InputBoxWrapper = ({
inputProps={{ "data-index": index }}
/>
</div>
{tooltip !== "" && (
<div className={classes.tooltipContainer}>
<Tooltip title={tooltip} placement="left">
<HelpIcon />
</Tooltip>
</div>
)}
</Grid>
</React.Fragment>
);

View File

@@ -23,9 +23,9 @@ import {
createStyles,
Theme,
withStyles,
makeStyles
makeStyles,
} from "@material-ui/core/styles";
import { fieldBasic } from "../common/styleLibrary";
import { fieldBasic, tooltipHelper } from "../common/styleLibrary";
import HelpIcon from "@material-ui/icons/Help";
export interface SelectorTypes {
@@ -48,22 +48,23 @@ interface RadioGroupProps {
const styles = (theme: Theme) =>
createStyles({
...fieldBasic,
...tooltipHelper,
radioBoxContainer: {
flexGrow: 1
}
flexGrow: 1,
},
});
const radioStyles = makeStyles({
root: {
"&:hover": {
backgroundColor: "transparent"
}
backgroundColor: "transparent",
},
},
icon: {
borderRadius: "100%",
width: 14,
height: 14,
border: "1px solid #000"
border: "1px solid #000",
},
checkedIcon: {
borderRadius: "100%",
@@ -81,9 +82,9 @@ const radioStyles = makeStyles({
position: "absolute",
backgroundColor: "#000",
top: 2,
left: 2
}
}
left: 2,
},
},
});
const RadioButton = (props: RadioProps) => {
@@ -110,14 +111,22 @@ export const RadioGroupSelector = ({
onChange,
tooltip = "",
classes,
displayInColumn = false
displayInColumn = false,
}: RadioGroupProps) => {
return (
<React.Fragment>
<Grid item xs={12} className={classes.fieldContainer}>
<InputLabel htmlFor={id} className={classes.inputLabel}>
{label}
<span>{label}</span>
{tooltip !== "" && (
<div className={classes.tooltipContainer}>
<Tooltip title={tooltip} placement="top-start">
<HelpIcon className={classes.tooltip} />
</Tooltip>
</div>
)}
</InputLabel>
<div className={classes.radioBoxContainer}>
<RadioGroup
aria-label={id}
@@ -127,7 +136,7 @@ export const RadioGroupSelector = ({
onChange={onChange}
row={!displayInColumn}
>
{selectorOptions.map(selectorOption => {
{selectorOptions.map((selectorOption) => {
return (
<FormControlLabel
key={`rd-${name}-${selectorOption.value}`}
@@ -139,13 +148,6 @@ export const RadioGroupSelector = ({
})}
</RadioGroup>
</div>
{tooltip !== "" && (
<div>
<Tooltip title={tooltip} placement="left">
<HelpIcon />
</Tooltip>
</div>
)}
</Grid>
</React.Fragment>
);

View File

@@ -20,10 +20,12 @@ import {
InputLabel,
MenuItem,
Select,
InputBase
InputBase,
Tooltip,
} from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { fieldBasic } from "../common/styleLibrary";
import { fieldBasic, tooltipHelper } from "../common/styleLibrary";
import HelpIcon from "@material-ui/icons/Help";
interface selectorTypes {
label: string;
@@ -36,6 +38,7 @@ interface SelectProps {
label: string;
id: string;
name: string;
tooltip?: string;
onChange: (
e: React.ChangeEvent<{ name?: string | undefined; value: unknown }>
) => void;
@@ -44,15 +47,20 @@ interface SelectProps {
const styles = (theme: Theme) =>
createStyles({
...fieldBasic
...fieldBasic,
...tooltipHelper,
inputLabel: {
...fieldBasic.inputLabel,
width: 116,
},
});
const SelectStyled = withStyles((theme: Theme) =>
createStyles({
root: {
"label + &": {
marginTop: theme.spacing(3)
}
marginTop: theme.spacing(3),
},
},
input: {
borderRadius: 0,
@@ -62,12 +70,12 @@ const SelectStyled = withStyles((theme: Theme) =>
padding: "11px 20px",
border: "1px solid #c4c4c4",
"&:hover": {
borderColor: "#393939"
borderColor: "#393939",
},
"&:focus": {
backgroundColor: "#fff"
}
}
backgroundColor: "#fff",
},
},
})
)(InputBase);
@@ -78,13 +86,21 @@ const SelectWrapper = ({
onChange,
options,
label,
value
tooltip = "",
value,
}: SelectProps) => {
return (
<React.Fragment>
<Grid item xs={12} className={classes.fieldContainer}>
<InputLabel htmlFor={id} className={classes.inputLabel}>
{label}
<span>{label}</span>
{tooltip !== "" && (
<div className={classes.tooltipContainer}>
<Tooltip title={tooltip} placement="top-start">
<HelpIcon className={classes.tooltip} />
</Tooltip>
</div>
)}
</InputLabel>
<FormControl variant="outlined" fullWidth>
<Select
@@ -94,7 +110,7 @@ const SelectWrapper = ({
onChange={onChange}
input={<SelectStyled />}
>
{options.map(option => (
{options.map((option) => (
<MenuItem
value={option.value}
key={`select-${name}-${option.label}`}

View File

@@ -19,17 +19,45 @@
export const fieldBasic = {
inputLabel: {
fontWeight: 500,
marginRight: 16,
minWidth: 90,
marginRight: 10,
width: 100,
fontSize: 14,
color: "#393939"
color: "#393939",
textAlign: "right" as const,
display: "flex",
textOverflow: "ellipsis",
overflow: "hidden",
justifyContent: "flex-end",
"& span": {
display: "flex",
alignItems: "center",
},
},
fieldContainer: {
display: "flex",
alignItems: "center",
marginBottom: 10
marginBottom: 10,
},
tooltipContainer: {
marginLeft: 5
}
marginLeft: 5,
display: "flex",
alignItems: "center",
},
};
export const modalBasic = {
formScrollable: {
maxHeight: "calc(100vh - 300px)" as const,
overflowY: "auto" as const,
marginBottom: 25,
},
formSlider: {
marginLeft: 0,
},
};
export const tooltipHelper = {
tooltip: {
fontSize: 18,
},
};

View File

@@ -17,9 +17,10 @@
import React, { useEffect, useState } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Grid from "@material-ui/core/Grid";
import { IElementValue, KVField } from "./types";
import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import RadioGroupSelector from "../Common/FormComponents/RadioGroupSelector/RadioGroupSelector";
import { IElementValue, KVField } from "./types";
import CSVMultiSelector from "../Common/FormComponents/CSVMultiSelector/CSVMultiSelector";
interface IConfGenericProps {
@@ -29,7 +30,10 @@ interface IConfGenericProps {
classes: any;
}
const styles = (theme: Theme) => createStyles({});
const styles = (theme: Theme) =>
createStyles({
...modalBasic,
});
// Function to get defined values,
//we make this because the backed sometimes don't return all the keys when there is an initial configuration
@@ -41,7 +45,7 @@ export const valueDef = (
let defValue = type === "on|off" ? "false" : "";
if (defaults.length > 0) {
const storedConfig = defaults.find(element => element.key === key);
const storedConfig = defaults.find((element) => element.key === key);
if (storedConfig) {
defValue = storedConfig.value;
@@ -55,7 +59,7 @@ const ConfTargetGeneric = ({
onChange,
fields,
defaultVals,
classes
classes,
}: IConfGenericProps) => {
const [valueHolder, setValueHolder] = useState<IElementValue[]>([]);
const fieldsElements = !fields ? [] : fields;
@@ -64,10 +68,10 @@ const ConfTargetGeneric = ({
// Effect to create all the values to hold
useEffect(() => {
const values: IElementValue[] = [];
fields.forEach(field => {
fields.forEach((field) => {
const stateInsert: IElementValue = {
key: field.name,
value: valueDef(field.name, field.type, defValList)
value: valueDef(field.name, field.type, defValList),
};
values.push(stateInsert);
});
@@ -102,7 +106,7 @@ const ConfTargetGeneric = ({
}
selectorOptions={[
{ label: "On", value: "true" },
{ label: "Off", value: "false" }
{ label: "Off", value: "false" },
]}
/>
);
@@ -137,18 +141,14 @@ const ConfTargetGeneric = ({
return (
<Grid container>
{fieldsElements.map((field, item) => (
<React.Fragment key={field.name}>
<Grid item xs={12}>
{fieldDefinition(field, item)}
</Grid>
</React.Fragment>
))}
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<br />
<Grid xs={12} item className={classes.formScrollable}>
{fieldsElements.map((field, item) => (
<React.Fragment key={field.name}>
<Grid item xs={12}>
{fieldDefinition(field, item)}
</Grid>
</React.Fragment>
))}
</Grid>
</Grid>
);

View File

@@ -21,13 +21,17 @@ import Grid from "@material-ui/core/Grid";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import RadioGroupSelector from "../../Common/FormComponents/RadioGroupSelector/RadioGroupSelector";
import { IElementValue } from "../types";
import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
interface IConfMySqlProps {
onChange: (newValue: IElementValue[]) => void;
classes: any;
}
const styles = (theme: Theme) => createStyles({});
const styles = (theme: Theme) =>
createStyles({
...modalBasic,
});
const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
//Local States
@@ -88,7 +92,7 @@ const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
{ key: "format", value: format },
{ key: "queue_dir", value: queueDir },
{ key: "queue_limit", value: queueLimit },
{ key: "comment", value: comment }
{ key: "comment", value: comment },
];
onChange(formValues);
@@ -101,7 +105,7 @@ const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
}, [user, dbName, password, port, host, setDsnString, configToDsnString]);
return (
<Grid container>
<Grid container className={classes.formScrollable}>
<Grid item xs={12}>
<FormControlLabel
control={
@@ -119,7 +123,7 @@ const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
"port",
"dbname",
"user",
"password"
"password",
]);
setHostname(kv.get("host") ? kv.get("host") + "" : "");
setPort(kv.get("port") ? kv.get("port") + "" : "");
@@ -137,6 +141,7 @@ const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
/>
}
label="Enter DSN String"
className={classes.formSlider}
/>
</Grid>
{useDsnString ? (
@@ -232,13 +237,13 @@ const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
id="format"
name="format"
label="Format"
onChange={e => {
onChange={(e) => {
setFormat(e.target.value);
}}
tooltip="'namespace' reflects current bucket/object list and 'access' reflects a journal of object operations, defaults to 'namespace'"
selectorOptions={[
{ label: "Namespace", value: "namespace" },
{ label: "Access", value: "access" }
{ label: "Access", value: "access" },
]}
/>
</Grid>
@@ -279,12 +284,6 @@ const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
}}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<br />
</Grid>
</Grid>
);
};

View File

@@ -22,13 +22,17 @@ import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBo
import RadioGroupSelector from "../../Common/FormComponents/RadioGroupSelector/RadioGroupSelector";
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
import { IElementValue } from "../types";
import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
interface IConfPostgresProps {
onChange: (newValue: IElementValue[]) => void;
classes: any;
}
const styles = (theme: Theme) => createStyles({});
const styles = (theme: Theme) =>
createStyles({
...modalBasic,
});
const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
//Local States
@@ -136,7 +140,7 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
{ key: "format", value: format },
{ key: "queue_dir", value: queueDir },
{ key: "queue_limit", value: queueLimit },
{ key: "comment", value: comment }
{ key: "comment", value: comment },
];
onChange(formValues);
@@ -148,7 +152,7 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
queueDir,
queueLimit,
comment,
onChange
onChange,
]);
useEffect(() => {
@@ -162,11 +166,11 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
sslMode,
host,
setConnectionString,
configToString
configToString,
]);
return (
<Grid container>
<Grid container className={classes.formScrollable}>
<Grid item xs={12}>
<FormControlLabel
control={
@@ -185,7 +189,7 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
"dbname",
"user",
"password",
"sslmode"
"sslmode",
]);
setHostname(kv.get("host") ? kv.get("host") + "" : "");
setPort(kv.get("port") ? kv.get("port") + "" : "");
@@ -206,6 +210,7 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
/>
}
label="Enter Connection String"
className={classes.formSlider}
/>
</Grid>
{useConnectionString ? (
@@ -273,7 +278,7 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
{ label: "Require", value: "require" },
{ label: "Disable", value: "disable" },
{ label: "Verify CA", value: "verify-ca" },
{ label: "Verify Full", value: "verify-full" }
{ label: "Verify Full", value: "verify-full" },
]}
/>
</Grid>
@@ -320,13 +325,13 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
id="format"
name="format"
label="Format"
onChange={e => {
onChange={(e) => {
setFormat(e.target.value);
}}
tooltip="'namespace' reflects current bucket/object list and 'access' reflects a journal of object operations, defaults to 'namespace'"
selectorOptions={[
{ label: "Namespace", value: "namespace" },
{ label: "Access", value: "access" }
{ label: "Access", value: "access" },
]}
/>
</Grid>
@@ -367,12 +372,6 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
}}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<br />
</Grid>
</Grid>
);
};

View File

@@ -16,35 +16,37 @@
import React, { useCallback, useEffect, useState } from "react";
import get from "lodash/get";
import { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Button, LinearProgress } from "@material-ui/core";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import api from "../../../../common/api";
import { serverNeedsRestart } from "../../../../actions";
import { connect } from "react-redux";
import ConfTargetGeneric from "../ConfTargetGeneric";
import { serverNeedsRestart } from "../../../../actions";
import { fieldBasic } from "../../Common/FormComponents/common/styleLibrary";
import { fieldsConfigurations, removeEmptyFields } from "../utils";
import { IConfigurationElement, IElementValue } from "../types";
const styles = (theme: Theme) =>
createStyles({
...fieldBasic,
errorBlock: {
color: "red"
color: "red",
},
strongText: {
fontWeight: 700
fontWeight: 700,
},
keyName: {
marginLeft: 5
marginLeft: 5,
},
buttonContainer: {
textAlign: "right"
textAlign: "right",
},
logoButton: {
height: "80px"
}
height: "80px",
},
});
interface IAddNotificationEndpointProps {
@@ -60,7 +62,7 @@ const EditConfiguration = ({
closeModalAndRefresh,
serverNeedsRestart,
selectedConfiguration,
classes
classes,
}: IAddNotificationEndpointProps) => {
//Local States
const [valuesObj, setValueObj] = useState<IElementValue[]>([]);
@@ -75,11 +77,11 @@ const EditConfiguration = ({
if (configId) {
api
.invoke("GET", `/api/v1/configs/${configId}`)
.then(res => {
.then((res) => {
const keyVals = get(res, "key_values", []);
setConfigValues(keyVals);
})
.catch(err => {
.catch((err) => {
setLoadingConfig(false);
setErrorConfig(err);
});
@@ -90,7 +92,7 @@ const EditConfiguration = ({
useEffect(() => {
if (saving) {
const payload = {
key_values: removeEmptyFields(valuesObj)
key_values: removeEmptyFields(valuesObj),
};
api
.invoke(
@@ -98,14 +100,14 @@ const EditConfiguration = ({
`/api/v1/configs/${selectedConfiguration.configuration_id}`,
payload
)
.then(res => {
.then((res) => {
setSaving(false);
setErrorConfig("");
serverNeedsRestart(true);
closeModalAndRefresh();
})
.catch(err => {
.catch((err) => {
setSaving(false);
setErrorConfig(err);
});
@@ -115,7 +117,7 @@ const EditConfiguration = ({
serverNeedsRestart,
selectedConfiguration,
valuesObj,
closeModalAndRefresh
closeModalAndRefresh,
]);
//Fetch Actions
@@ -125,7 +127,7 @@ const EditConfiguration = ({
};
const onValueChange = useCallback(
newValue => {
(newValue) => {
setValueObj(newValue);
},
[setValueObj]
@@ -157,12 +159,11 @@ const EditConfiguration = ({
onChange={onValueChange}
defaultVals={configValues}
/>
<Grid item xs={3} className={classes.buttonContainer}>
<Grid item xs={12} className={classes.buttonContainer}>
<Button
type="submit"
variant="contained"
color="primary"
fullWidth
disabled={saving}
>
Save

View File

@@ -20,7 +20,7 @@ import {
createStyles,
StyledProps,
Theme,
withStyles
withStyles,
} from "@material-ui/core/styles";
import CssBaseline from "@material-ui/core/CssBaseline";
import Drawer from "@material-ui/core/Drawer";
@@ -36,14 +36,14 @@ import {
RouteComponentProps,
Router,
Switch,
withRouter
withRouter,
} from "react-router-dom";
import { connect } from "react-redux";
import { AppState } from "../../store";
import {
serverIsLoading,
serverNeedsRestart,
setMenuOpen
setMenuOpen,
} from "../../actions";
import { ThemedComponentProps } from "@material-ui/core/styles/withTheme";
import Buckets from "./Buckets/Buckets";
@@ -63,6 +63,7 @@ import { Button, LinearProgress } from "@material-ui/core";
import WebhookPanel from "./Configurations/ConfigurationPanels/WebhookPanel";
import Trace from "./Trace/Trace";
import Logs from "./Logs/Logs";
import Watch from "./Watch/Watch";
function Copyright() {
return (
@@ -82,43 +83,43 @@ const drawerWidth = 254;
const styles = (theme: Theme) =>
createStyles({
root: {
display: "flex"
display: "flex",
},
toolbar: {
background: theme.palette.background.default,
color: "black",
paddingRight: 24 // keep right padding when drawer closed
paddingRight: 24, // keep right padding when drawer closed
},
toolbarIcon: {
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
padding: "0 8px",
...theme.mixins.toolbar
...theme.mixins.toolbar,
},
appBar: {
zIndex: theme.zIndex.drawer + 1,
transition: theme.transitions.create(["width", "margin"], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
})
duration: theme.transitions.duration.leavingScreen,
}),
},
appBarShift: {
marginLeft: drawerWidth,
width: `calc(100% - ${drawerWidth}px)`,
transition: theme.transitions.create(["width", "margin"], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen
})
duration: theme.transitions.duration.enteringScreen,
}),
},
menuButton: {
marginRight: 36
marginRight: 36,
},
menuButtonHidden: {
display: "none"
display: "none",
},
title: {
flexGrow: 1
flexGrow: 1,
},
drawerPaper: {
position: "relative",
@@ -126,40 +127,40 @@ const styles = (theme: Theme) =>
width: drawerWidth,
transition: theme.transitions.create("width", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen
})
duration: theme.transitions.duration.enteringScreen,
}),
},
drawerPaperClose: {
overflowX: "hidden",
transition: theme.transitions.create("width", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
duration: theme.transitions.duration.leavingScreen,
}),
width: theme.spacing(7),
[theme.breakpoints.up("sm")]: {
width: theme.spacing(9)
}
width: theme.spacing(9),
},
},
appBarSpacer: {
height: "5px"
height: "5px",
},
content: {
flexGrow: 1,
height: "100vh",
overflow: "auto"
overflow: "auto",
},
container: {
paddingTop: theme.spacing(4),
paddingBottom: theme.spacing(4)
paddingBottom: theme.spacing(4),
},
paper: {
padding: theme.spacing(2),
display: "flex",
overflow: "auto",
flexDirection: "column"
flexDirection: "column",
},
fixedHeight: {
minHeight: 240
minHeight: 240,
},
warningBar: {
background: theme.palette.primary.main,
@@ -167,20 +168,20 @@ const styles = (theme: Theme) =>
heigh: "60px",
widht: "100%",
lineHeight: "60px",
textAlign: "center"
}
textAlign: "center",
},
});
const mapState = (state: AppState) => ({
open: state.system.sidebarOpen,
needsRestart: state.system.serverNeedsRestart,
isServerLoading: state.system.serverIsLoading
isServerLoading: state.system.serverIsLoading,
});
const connector = connect(mapState, {
setMenuOpen,
serverNeedsRestart,
serverIsLoading
serverIsLoading,
});
interface IConsoleProps {
@@ -196,14 +197,14 @@ interface IConsoleProps {
class Console extends React.Component<
IConsoleProps & RouteComponentProps & StyledProps & ThemedComponentProps
> {
> {
componentDidMount(): void {
api
.invoke("GET", `/api/v1/session`)
.then(res => {
.then((res) => {
console.log(res);
})
.catch(err => {
.catch((err) => {
storage.removeItem("token");
history.push("/");
});
@@ -213,13 +214,13 @@ class Console extends React.Component<
this.props.serverIsLoading(true);
api
.invoke("POST", "/api/v1/service/restart", {})
.then(res => {
.then((res) => {
console.log("success restarting service");
console.log(res);
this.props.serverIsLoading(false);
this.props.serverNeedsRestart(false);
})
.catch(err => {
.catch((err) => {
this.props.serverIsLoading(false);
console.log("failure restarting service");
console.log(err);
@@ -234,7 +235,7 @@ class Console extends React.Component<
<Drawer
variant="permanent"
classes={{
paper: clsx(classes.drawerPaper, !open && classes.drawerPaperClose)
paper: clsx(classes.drawerPaper, !open && classes.drawerPaperClose),
}}
open={open}
>
@@ -261,20 +262,20 @@ class Console extends React.Component<
<LinearProgress />
</React.Fragment>
) : (
<React.Fragment>
The instance needs to be restarted for configuration changes
<React.Fragment>
The instance needs to be restarted for configuration changes
to take effect.{" "}
<Button
color="secondary"
size="small"
onClick={() => {
this.restartServer();
}}
>
Restart
<Button
color="secondary"
size="small"
onClick={() => {
this.restartServer();
}}
>
Restart
</Button>
</React.Fragment>
)}
</React.Fragment>
)}
</div>
)}
<div className={classes.appBarSpacer} />
@@ -306,6 +307,7 @@ class Console extends React.Component<
<Route exact path="/webhook/audit" component={WebhookPanel} />
<Route exct path="/trace" component={Trace} />
<Route exct path="/logs" component={Logs} />
<Route exct path="/watch" component={Watch} />
<Route exact path="/">
<Redirect to="/dashboard" />
</Route>

View File

@@ -26,86 +26,87 @@ import ViewHeadlineIcon from "@material-ui/icons/ViewHeadline";
import { Usage } from "./types";
import api from "../../../common/api";
import { niceBytes } from "../../../common/utils";
import { LinearProgress } from "@material-ui/core";
const styles = (theme: Theme) =>
createStyles({
root: {
display: "flex"
display: "flex",
},
toolbar: {
paddingRight: 24 // keep right padding when drawer closed
paddingRight: 24, // keep right padding when drawer closed
},
toolbarIcon: {
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
padding: "0 8px",
...theme.mixins.toolbar
...theme.mixins.toolbar,
},
appBar: {
zIndex: theme.zIndex.drawer + 1,
transition: theme.transitions.create(["width", "margin"], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
})
duration: theme.transitions.duration.leavingScreen,
}),
},
menuButton: {
marginRight: 36
marginRight: 36,
},
menuButtonHidden: {
display: "none"
display: "none",
},
title: {
flexGrow: 1
flexGrow: 1,
},
drawerPaperClose: {
overflowX: "hidden",
transition: theme.transitions.create("width", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
duration: theme.transitions.duration.leavingScreen,
}),
width: theme.spacing(7),
[theme.breakpoints.up("sm")]: {
width: theme.spacing(9)
}
width: theme.spacing(9),
},
},
appBarSpacer: theme.mixins.toolbar,
content: {
flexGrow: 1,
height: "100vh",
overflow: "auto"
overflow: "auto",
},
container: {
paddingBottom: theme.spacing(4),
"& h6": {
color: "#777777",
fontSize: 14
fontSize: 14,
},
"& p": {
"& span": {
fontSize: 16
}
}
fontSize: 16,
},
},
},
paper: {
padding: theme.spacing(2),
display: "flex",
overflow: "auto",
flexDirection: "column"
flexDirection: "column",
},
fixedHeight: {
minHeight: 240
minHeight: 240,
},
consumptionValue: {
color: "#000000",
fontSize: "60px",
fontWeight: "bold"
fontWeight: "bold",
},
icon: {
marginRight: 10,
color: "#777777"
}
color: "#777777",
},
});
interface IDashboardProps {
@@ -115,7 +116,7 @@ interface IDashboardProps {
const Dashboard = ({ classes }: IDashboardProps) => {
const fixedHeightPaper = clsx(classes.paper, classes.fixedHeight);
const [usage, setUsage] = useState<Usage | null>(null);
const [loading, isLoading] = useState<boolean>(true);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
useEffect(() => {
@@ -130,11 +131,11 @@ const Dashboard = ({ classes }: IDashboardProps) => {
.then((res: Usage) => {
setUsage(res);
setError("");
isLoading(false);
setLoading(false);
})
.catch(err => {
.catch((err) => {
setError(err);
isLoading(false);
setLoading(false);
});
};
const prettyUsage = (usage: string | undefined) => {
@@ -143,7 +144,6 @@ const Dashboard = ({ classes }: IDashboardProps) => {
}
return niceBytes(usage);
};
const units = ["bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const prettyNumber = (usage: number | undefined) => {
if (usage === undefined) {
@@ -161,51 +161,59 @@ const Dashboard = ({ classes }: IDashboardProps) => {
<Typography variant="h2">MinIO Console</Typography>
</Grid>
{error !== "" && <Grid container>{error}</Grid>}
<Grid item xs={12} md={4} lg={4}>
<Paper className={fixedHeightPaper}>
<Grid container direction="row" alignItems="center">
<Grid item className={classes.icon}>
<ViewHeadlineIcon />
</Grid>
<Grid item>
<Typography variant="h6">Total Buckets</Typography>
</Grid>
{loading ? (
<Grid item xs={12} md={12} lg={12}>
<LinearProgress />
</Grid>
) : (
<React.Fragment>
<Grid item xs={12} md={4} lg={4}>
<Paper className={fixedHeightPaper}>
<Grid container direction="row" alignItems="center">
<Grid item className={classes.icon}>
<ViewHeadlineIcon />
</Grid>
<Grid item>
<Typography variant="h6">Total Buckets</Typography>
</Grid>
</Grid>
<Typography className={classes.consumptionValue}>
{usage ? prettyNumber(usage.buckets) : 0}
</Typography>
</Paper>
</Grid>
<Typography className={classes.consumptionValue}>
{usage ? prettyNumber(usage.buckets) : 0}
</Typography>
</Paper>
</Grid>
<Grid item xs={12} md={4} lg={4}>
<Paper className={fixedHeightPaper}>
<Grid container direction="row" alignItems="center">
<Grid item className={classes.icon}>
<NetworkCheckIcon />
</Grid>
<Grid item>
<Typography variant="h6"> Total Objects</Typography>
</Grid>
<Grid item xs={12} md={4} lg={4}>
<Paper className={fixedHeightPaper}>
<Grid container direction="row" alignItems="center">
<Grid item className={classes.icon}>
<NetworkCheckIcon />
</Grid>
<Grid item>
<Typography variant="h6"> Total Objects</Typography>
</Grid>
</Grid>
<Typography className={classes.consumptionValue}>
{usage ? prettyNumber(usage.objects) : 0}
</Typography>
</Paper>
</Grid>
<Typography className={classes.consumptionValue}>
{usage ? prettyNumber(usage.objects) : 0}
</Typography>
</Paper>
</Grid>
<Grid item xs={12} md={4} lg={4}>
<Paper className={fixedHeightPaper}>
<Grid container direction="row" alignItems="center">
<Grid item className={classes.icon}>
<PieChartIcon />
</Grid>
<Grid item>
<Typography variant="h6">Usage</Typography>
</Grid>
<Grid item xs={12} md={4} lg={4}>
<Paper className={fixedHeightPaper}>
<Grid container direction="row" alignItems="center">
<Grid item className={classes.icon}>
<PieChartIcon />
</Grid>
<Grid item>
<Typography variant="h6">Usage</Typography>
</Grid>
</Grid>
<Typography className={classes.consumptionValue}>
{usage ? prettyUsage(usage.usage + "") : 0}
</Typography>
</Paper>
</Grid>
<Typography className={classes.consumptionValue}>
{usage ? prettyUsage(usage.usage + "") : 0}
</Typography>
</Paper>
</Grid>
</React.Fragment>
)}
</Grid>
</Grid>
</React.Fragment>

View File

@@ -19,6 +19,7 @@ import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Button, LinearProgress } from "@material-ui/core";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
import api from "../../../common/api";
import UsersSelectors from "./UsersSelectors";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
@@ -41,24 +42,25 @@ interface MainGroupProps {
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
color: "red",
},
strongText: {
fontWeight: 700
fontWeight: 700,
},
keyName: {
marginLeft: 5
marginLeft: 5,
},
buttonContainer: {
textAlign: "right"
}
textAlign: "right",
},
...modalBasic,
});
const AddGroup = ({
open,
selectedGroup,
closeModalAndRefresh,
classes
classes,
}: IGroupProps) => {
//Local States
const [groupName, setGroupName] = useState<string>("");
@@ -86,14 +88,14 @@ const AddGroup = ({
.invoke("PUT", `/api/v1/groups/${groupName}`, {
group: groupName,
members: selectedUsers,
status: groupEnabled
status: groupEnabled,
})
.then(res => {
.then((res) => {
isSaving(false);
setError("");
closeModalAndRefresh();
})
.catch(err => {
.catch((err) => {
isSaving(false);
setError(err);
});
@@ -101,14 +103,14 @@ const AddGroup = ({
api
.invoke("POST", "/api/v1/groups", {
group: groupName,
members: selectedUsers
members: selectedUsers,
})
.then(res => {
.then((res) => {
isSaving(false);
setError("");
closeModalAndRefresh();
})
.catch(err => {
.catch((err) => {
isSaving(false);
setError(err);
});
@@ -122,7 +124,7 @@ const AddGroup = ({
selectedUsers,
groupEnabled,
selectedGroup,
closeModalAndRefresh
closeModalAndRefresh,
]);
useEffect(() => {
@@ -135,7 +137,7 @@ const AddGroup = ({
setGroupName(res.name);
setSelectedUsers(res.members);
})
.catch(err => {
.catch((err) => {
setError(err);
isLoadingGroup(false);
});
@@ -159,62 +161,61 @@ const AddGroup = ({
>
<form noValidate autoComplete="off" onSubmit={setSaving}>
<Grid container>
{addError !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
</Grid>
)}
<Grid item xs={12} className={classes.formScrollable}>
{addError !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
</Grid>
)}
{selectedGroup !== null ? (
<React.Fragment>
<Grid item xs={12}>
<RadioGroupSelector
currentSelection={groupEnabled}
id="group-status"
name="group-status"
label="Status"
onChange={e => {
setGroupEnabled(e.target.value);
}}
selectorOptions={[
{ label: "Enabled", value: "enabled" },
{ label: "Disabled", value: "disabled" }
]}
/>
</Grid>
</React.Fragment>
) : (
<React.Fragment>
<Grid item xs={12}>
<InputBoxWrapper
id="group-name"
name="group-name"
label="Name"
value={groupName}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setGroupName(e.target.value);
}}
/>
</Grid>
</React.Fragment>
)}
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<UsersSelectors
selectedUsers={selectedUsers}
setSelectedUsers={setSelectedUsers}
/>
</Grid>
<Grid item xs={12}>
<br />
{selectedGroup !== null ? (
<React.Fragment>
<Grid item xs={12}>
<RadioGroupSelector
currentSelection={groupEnabled}
id="group-status"
name="group-status"
label="Status"
onChange={(e) => {
setGroupEnabled(e.target.value);
}}
selectorOptions={[
{ label: "Enabled", value: "enabled" },
{ label: "Disabled", value: "disabled" },
]}
/>
</Grid>
</React.Fragment>
) : (
<React.Fragment>
<Grid item xs={12}>
<InputBoxWrapper
id="group-name"
name="group-name"
label="Name"
value={groupName}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setGroupName(e.target.value);
}}
/>
</Grid>
</React.Fragment>
)}
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<UsersSelectors
selectedUsers={selectedUsers}
setSelectedUsers={setSelectedUsers}
/>
</Grid>
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>
<Button

View File

@@ -15,15 +15,13 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useEffect } from "react";
import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket";
import storage from "local-storage-fallback";
import { AppState } from "../../../store";
import { connect } from "react-redux";
import { logMessageReceived, logResetMessages } from "./actions";
import { LogMessage } from "./types";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { niceBytes } from "../../../common/utils";
import Ansi from "ansi-to-react";
import { isNull, isNullOrUndefined } from "util";
import { timeFromDate } from "../../../common/utils";
import { isNullOrUndefined } from "util";
import { wsProtocol } from "../../../utils/wsUtils";
const styles = (theme: Theme) =>
@@ -34,31 +32,28 @@ const styles = (theme: Theme) =>
overflow: "auto",
"& ul": {
margin: "4px",
padding: "0px"
padding: "0px",
},
"& ul li": {
listStyle: "none",
margin: "0px",
padding: "0px",
borderBottom: "1px solid #dedede"
}
borderBottom: "1px solid #dedede",
},
},
tab: {
padding: "25px"
},
ansiblue: {
color: "blue"
padding: "25px",
},
logerror: {
color: "red"
color: "#A52A2A",
},
logerror_tab: {
color: "red",
padding: "25px"
color: "#A52A2A",
padding: "25px",
},
ansidefault: {
color: "black"
}
color: "black",
},
});
interface ILogs {
@@ -72,7 +67,7 @@ const Logs = ({
classes,
logMessageReceived,
logResetMessages,
messages
messages,
}: ILogs) => {
useEffect(() => {
logResetMessages();
@@ -114,14 +109,6 @@ const Logs = ({
}
}, [logMessageReceived]);
const timeFromdate = (d: Date) => {
let h = d.getHours() < 10 ? `0${d.getHours()}` : `${d.getHours()}`;
let m = d.getMinutes() < 10 ? `0${d.getMinutes()}` : `${d.getMinutes()}`;
let s = d.getSeconds() < 10 ? `0${d.getSeconds()}` : `${d.getSeconds()}`;
return `${h}:${m}:${s}:${d.getMilliseconds()}`;
};
// replaces a character of a string with other at a given index
const replaceWeirdChar = (
origString: string,
@@ -135,23 +122,6 @@ const Logs = ({
return newString;
};
const colorify = (str: string) => {
// matches strings starting like: `[34mEndpoint: [0m`
const colorRegex = /(\[[0-9]+m)(.*?)(\[0+m)/g;
let matches = colorRegex.exec(str);
if (!isNullOrUndefined(matches)) {
let start_color = matches[1];
let text = matches[2];
if (start_color === "[34m") {
return <span className={classes.ansiblue}>{text}</span>;
}
if (start_color === "[1m") {
return <span className={classes.ansidarkblue}>{text}</span>;
}
}
};
const renderError = (logElement: LogMessage) => {
let errorElems = [];
if (!isNullOrUndefined(logElement.error)) {
@@ -166,7 +136,7 @@ const Logs = ({
errorElems.push(
<li key={`time-${logElement.key}`}>
<span className={classes.logerror}>
Time: {timeFromdate(logElement.time)}
Time: {timeFromDate(logElement.time)}
</span>
</li>
);
@@ -241,36 +211,22 @@ const Logs = ({
const renderLog = (logElement: LogMessage) => {
let logMessage = logElement.ConsoleMsg;
// if somehow after the color code starts with unicode 10 = Line feed
// delete it
const regexInit = /(\[[0-9]+m)/g;
let match = regexInit.exec(logMessage);
if (match) {
if (logMessage.slice(match[0].length).codePointAt(1) == 10) {
logMessage = replaceWeirdChar(logMessage, "", match[0].length + 1);
}
}
// remove any non ascii characters, exclude any control codes
logMessage = logMessage.replace(/([^\x20-\x7F])/g, "");
// Select what to add color and what not to.
const colorRegex = /(\[[0-9]+m)(.*?)(\[0+m)/g;
let m = colorRegex.exec(logMessage);
// regex for terminal colors like e.g. `[31;4m `
const tColorRegex = /((\[[0-9;]+m))/g;
// get substring if there was a match for to split what
// is going to be colored and what not, here we add color
// only to the first match.
let substr = logMessage.slice(colorRegex.lastIndex);
substr = substr.replace(regexInit, "");
let substr = logMessage.replace(tColorRegex, "");
// strClean used for corner case when string has unicode 32 for
// space instead of normal space.
let strClean = logMessage.replace(regexInit, "");
// if starts with multiple spaces add padding
if (strClean.startsWith(" ") || strClean.codePointAt(1) === 32) {
if (substr.startsWith(" ")) {
return (
<li key={logElement.key}>
<span className={classes.tab}>
{colorify(logMessage)} {substr}
</span>
<span className={classes.tab}>{substr}</span>
</li>
);
} else if (!isNullOrUndefined(logElement.error)) {
@@ -280,9 +236,7 @@ const Logs = ({
// for all remaining set default class
return (
<li key={logElement.key}>
<span className={classes.ansidefault}>
{colorify(logMessage)} {substr}
</span>
<span className={classes.ansidefault}>{substr}</span>
</li>
);
}
@@ -293,7 +247,7 @@ const Logs = ({
<h1>Logs</h1>
<div className={classes.logList}>
<ul>
{messages.map(m => {
{messages.map((m) => {
return renderLog(m);
})}
</ul>
@@ -303,12 +257,12 @@ const Logs = ({
};
const mapState = (state: AppState) => ({
messages: state.logs.messages
messages: state.logs.messages,
});
const connector = connect(mapState, {
logMessageReceived: logMessageReceived,
logResetMessages: logResetMessages
logResetMessages: logResetMessages,
});
export default connector(withStyles(styles)(Logs));

View File

@@ -18,6 +18,7 @@ import React from "react";
import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import WebAssetIcon from "@material-ui/icons/WebAsset";
import CenterFocusWeakIcon from '@material-ui/icons/CenterFocusWeak';
import ListItemText from "@material-ui/core/ListItemText";
import { NavLink } from "react-router-dom";
import { Divider, Typography, withStyles } from "@material-ui/core";
@@ -33,7 +34,7 @@ import {
BucketsIcon,
DashboardIcon,
PermissionIcon,
UsersIcon
UsersIcon,
} from "../../icons";
import { createStyles, Theme } from "@material-ui/core/styles";
import PersonIcon from "@material-ui/icons/Person";
@@ -49,8 +50,8 @@ const styles = (theme: Theme) =>
marginBottom: "20px",
textAlign: "center",
"& img": {
width: "120px"
}
width: "120px",
},
},
menuList: {
"& .active": {
@@ -60,31 +61,31 @@ const styles = (theme: Theme) =>
background:
"transparent linear-gradient(90deg, #362585 0%, #362585 7%, #281B6F 39%, #1F1661 100%) 0% 0% no-repeat padding-box",
"& .MuiSvgIcon-root": {
color: "white"
}
color: "white",
},
},
"& .MuiListItem-root": {
marginTop: "16px"
marginTop: "16px",
},
paddingLeft: "30px",
"& .MuiSvgIcon-root": {
fontSize: "18px",
color: "#393939"
color: "#393939",
},
"& .MuiListItemIcon-root": {
minWidth: "40px"
minWidth: "40px",
},
"& .MuiTypography-root": {
fontSize: "14px"
fontSize: "14px",
},
"& .MuiListItem-gutters": {
paddingRight: "0px"
}
}
paddingRight: "0px",
},
},
});
const mapState = (state: AppState) => ({
open: state.system.loggedIn
open: state.system.loggedIn,
});
const connector = connect(mapState, { userLoggedIn });
@@ -132,6 +133,12 @@ class Menu extends React.Component<MenuProps> {
</ListItemIcon>
<ListItemText primary="Buckets" />
</ListItem>
<ListItem button component={NavLink} to="/watch">
<ListItemIcon>
<CenterFocusWeakIcon />
</ListItemIcon>
<ListItemText primary="Watch" />
</ListItem>
<Divider />
<ListItem component={Typography}>Admin</ListItem>
<ListItem button component={NavLink} to="/users">

View File

@@ -39,27 +39,27 @@ import {
notifyElasticsearch,
notifyWebhooks,
notifyNsq,
removeEmptyFields
removeEmptyFields,
} from "../Configurations/utils";
import { IElementValue } from "../Configurations/types";
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
color: "red",
},
strongText: {
fontWeight: 700
fontWeight: 700,
},
keyName: {
marginLeft: 5
marginLeft: 5,
},
buttonContainer: {
textAlign: "right"
textAlign: "right",
},
logoButton: {
height: "80px"
}
height: "80px",
},
});
interface IAddNotificationEndpointProps {
@@ -73,7 +73,7 @@ const AddNotificationEndpoint = ({
open,
closeModalAndRefresh,
serverNeedsRestart,
classes
classes,
}: IAddNotificationEndpointProps) => {
//Local States
const [service, setService] = useState<string>("");
@@ -86,18 +86,18 @@ const AddNotificationEndpoint = ({
useEffect(() => {
if (saving) {
const payload = {
key_values: removeEmptyFields(valuesArr)
key_values: removeEmptyFields(valuesArr),
};
api
.invoke("PUT", `/api/v1/configs/${service}`, payload)
.then(res => {
.then((res) => {
setSaving(false);
setError("");
serverNeedsRestart(true);
closeModalAndRefresh();
})
.catch(err => {
.catch((err) => {
setSaving(false);
setError(err);
});
@@ -111,7 +111,7 @@ const AddNotificationEndpoint = ({
};
const onValueChange = useCallback(
newValue => {
(newValue) => {
setValueArr(newValue);
},
[setValueArr]
@@ -342,14 +342,14 @@ const AddNotificationEndpoint = ({
</Grid>
)}
<form noValidate onSubmit={submitForm}>
{srvComponent}
<Grid item xs={3} className={classes.buttonContainer}>
<Grid item xs={12}>
{srvComponent}
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>
<Button
type="submit"
variant="contained"
color="primary"
fullWidth
disabled={saving}
>
Save

View File

@@ -24,6 +24,7 @@ import api from "../../../common/api";
import "codemirror/lib/codemirror.css";
import "codemirror/theme/material.css";
import { Policy } from "./types";
import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
@@ -32,18 +33,19 @@ require("codemirror/mode/javascript/javascript");
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
color: "red",
},
jsonPolicyEditor: {
minHeight: 400,
width: "100%"
width: "100%",
},
codeMirror: {
fontSize: 14
fontSize: 14,
},
buttonContainer: {
textAlign: "right"
}
textAlign: "right",
},
...modalBasic,
});
interface IAddPolicyProps {
@@ -65,7 +67,7 @@ class AddPolicy extends React.Component<IAddPolicyProps, IAddPolicyState> {
addLoading: false,
addError: "",
policyName: "",
policyDefinition: ""
policyDefinition: "",
};
addRecord(event: React.FormEvent) {
@@ -78,23 +80,23 @@ class AddPolicy extends React.Component<IAddPolicyProps, IAddPolicyState> {
api
.invoke("POST", "/api/v1/policies", {
name: policyName,
policy: policyDefinition
policy: policyDefinition,
})
.then(res => {
.then((res) => {
this.setState(
{
addLoading: false,
addError: ""
addError: "",
},
() => {
this.props.closeModalAndRefresh();
}
);
})
.catch(err => {
.catch((err) => {
this.setState({
addLoading: false,
addError: err
addError: err,
});
});
});
@@ -105,7 +107,7 @@ class AddPolicy extends React.Component<IAddPolicyProps, IAddPolicyState> {
if (policyEdit) {
this.setState({
policyName: policyEdit.name
policyName: policyEdit.name,
});
}
}
@@ -131,51 +133,50 @@ class AddPolicy extends React.Component<IAddPolicyProps, IAddPolicyState> {
}}
>
<Grid container>
{addError !== "" && (
<Grid item xs={12} className={classes.formScrollable}>
{addError !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
</Grid>
)}
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
<InputBoxWrapper
id="policy-name"
name="policy-name"
label="Policy Name"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ policyName: e.target.value });
}}
value={policyName}
disabled={!!policyEdit}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<CodeMirror
className={classes.codeMirror}
value={
policyEdit
? JSON.stringify(JSON.parse(policyEdit.policy), null, 4)
: ""
}
options={{
mode: "javascript",
lineNumbers: true,
}}
onChange={(editor, data, value) => {
this.setState({ policyDefinition: value });
}}
/>
</Grid>
)}
<Grid item xs={12}>
<InputBoxWrapper
id="policy-name"
name="policy-name"
label="Policy Name"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ policyName: e.target.value });
}}
value={policyName}
disabled={!!policyEdit}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<CodeMirror
className={classes.codeMirror}
value={
policyEdit
? JSON.stringify(JSON.parse(policyEdit.policy), null, 4)
: ""
}
options={{
mode: "javascript",
lineNumbers: true
}}
onChange={(editor, data, value) => {
this.setState({ policyDefinition: value });
}}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
{!policyEdit && (
<Grid item xs={12} className={classes.buttonContainer}>

View File

@@ -15,13 +15,12 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useEffect } from "react";
import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket";
import storage from "local-storage-fallback";
import { AppState } from "../../../store";
import { connect } from "react-redux";
import { traceMessageReceived, traceResetMessages } from "./actions";
import { TraceMessage } from "./types";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { niceBytes } from "../../../common/utils";
import { niceBytes, timeFromDate } from "../../../common/utils";
import { wsProtocol } from "../../../utils/wsUtils";
const styles = (theme: Theme) =>
@@ -92,14 +91,6 @@ const Trace = ({
}
}, [traceMessageReceived]);
const timeFromdate = (d: Date) => {
let h = d.getHours() < 10 ? `0${d.getHours()}` : `${d.getHours()}`;
let m = d.getMinutes() < 10 ? `0${d.getMinutes()}` : `${d.getMinutes()}`;
let s = d.getSeconds() < 10 ? `0${d.getSeconds()}` : `${d.getSeconds()}`;
return `${h}:${m}:${s}:${d.getMilliseconds()}`;
};
return (
<div>
<h1>Trace</h1>
@@ -108,7 +99,7 @@ const Trace = ({
{messages.map(m => {
return (
<li key={m.key}>
{timeFromdate(m.time)} - {m.api}[{m.statusCode} {m.statusMsg}]{" "}
{timeFromDate(m.time)} - {m.api}[{m.statusCode} {m.statusMsg}]{" "}
{m.api} {m.host} {m.client} {m.callStats.duration} {" "}
{niceBytes(m.callStats.rx + "")} {" "}
{niceBytes(m.callStats.tx + "")}

View File

@@ -18,6 +18,7 @@ import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Button, LinearProgress } from "@material-ui/core";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
import api from "../../../common/api";
import GroupsSelectors from "./GroupsSelectors";
import Title from "../../../common/Title";
@@ -33,24 +34,25 @@ interface IAddToGroup {
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
color: "red",
},
strongText: {
fontWeight: 700
fontWeight: 700,
},
keyName: {
marginLeft: 5
marginLeft: 5,
},
buttonContainer: {
textAlign: "right"
}
textAlign: "right",
},
...modalBasic,
});
const AddToGroup = ({
open,
checkedUsers,
closeModalAndRefresh,
classes
classes,
}: IAddToGroup) => {
//Local States
const [saving, isSaving] = useState<boolean>(false);
@@ -64,14 +66,14 @@ const AddToGroup = ({
api
.invoke("PUT", "/api/v1/users-groups-bulk", {
groups: selectedGroups,
users: checkedUsers
users: checkedUsers,
})
.then(res => {
.then((res) => {
isSaving(false);
setError("");
closeModalAndRefresh(true);
})
.catch(err => {
.catch((err) => {
isSaving(false);
setError(err);
});
@@ -86,7 +88,7 @@ const AddToGroup = ({
setError,
closeModalAndRefresh,
selectedGroups,
checkedUsers
checkedUsers,
]);
//Fetch Actions
@@ -106,35 +108,34 @@ const AddToGroup = ({
>
<form noValidate autoComplete="off" onSubmit={setSaving}>
<Grid container>
{updatingError !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{updatingError}
</Typography>
</Grid>
)}
<Grid item xs={12} className={classes.formScrollable}>
{updatingError !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{updatingError}
</Typography>
</Grid>
)}
<Grid item xs={12}>
<Title>Users to be altered</Title>
</Grid>
<Grid item xs={12}>
{checkedUsers.join(", ")}
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<GroupsSelectors
selectedGroups={selectedGroups}
setSelectedGroups={setSelectedGroups}
/>
</Grid>
<Grid item xs={12}>
<br />
<Grid item xs={12}>
<Title>Users to be altered</Title>
</Grid>
<Grid item xs={12}>
{checkedUsers.join(", ")}
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<GroupsSelectors
selectedGroups={selectedGroups}
setSelectedGroups={setSelectedGroups}
/>
</Grid>
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>
<Button

View File

@@ -19,8 +19,9 @@ import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import { Button, LinearProgress } from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import api from "../../../common/api";
import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
import { User } from "./types";
import api from "../../../common/api";
import GroupsSelectors from "./GroupsSelectors";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
@@ -29,17 +30,18 @@ import RadioGroupSelector from "../Common/FormComponents/RadioGroupSelector/Radi
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
color: "red",
},
strongText: {
fontWeight: 700
fontWeight: 700,
},
keyName: {
marginLeft: 5
marginLeft: 5,
},
buttonContainer: {
textAlign: "right"
}
textAlign: "right",
},
...modalBasic,
});
interface IAddUserContentProps {
@@ -68,7 +70,7 @@ class AddUserContent extends React.Component<
accessKey: "",
secretKey: "",
enabled: "enabled",
selectedGroups: []
selectedGroups: [],
};
componentDidMount(): void {
@@ -77,7 +79,7 @@ class AddUserContent extends React.Component<
this.setState({
accessKey: "",
secretKey: "",
selectedGroups: []
selectedGroups: [],
});
} else {
this.getUserInformation();
@@ -91,7 +93,7 @@ class AddUserContent extends React.Component<
addLoading,
secretKey,
selectedGroups,
enabled
enabled,
} = this.state;
const { selectedUser } = this.props;
if (addLoading) {
@@ -102,23 +104,23 @@ class AddUserContent extends React.Component<
api
.invoke("PUT", `/api/v1/users/${selectedUser.accessKey}`, {
status: enabled,
groups: selectedGroups
groups: selectedGroups,
})
.then(res => {
.then((res) => {
this.setState(
{
addLoading: false,
addError: ""
addError: "",
},
() => {
this.props.closeModalAndRefresh();
}
);
})
.catch(err => {
.catch((err) => {
this.setState({
addLoading: false,
addError: err
addError: err,
});
});
} else {
@@ -126,24 +128,24 @@ class AddUserContent extends React.Component<
.invoke("POST", "/api/v1/users", {
accessKey,
secretKey,
groups: selectedGroups
groups: selectedGroups,
})
.then(res => {
.then((res) => {
this.setState(
{
addLoading: false,
addError: ""
addError: "",
},
() => {
this.props.closeModalAndRefresh();
}
);
})
.catch(err => {
.catch((err) => {
console.log(err);
this.setState({
addLoading: false,
addError: err
addError: err,
});
});
}
@@ -159,19 +161,19 @@ class AddUserContent extends React.Component<
api
.invoke("GET", `/api/v1/users/${selectedUser.accessKey}`)
.then(res => {
.then((res) => {
this.setState({
addLoading: false,
addError: "",
accessKey: res.accessKey,
selectedGroups: res.memberOf || [],
enabled: res.status
enabled: res.status,
});
})
.catch(err => {
.catch((err) => {
this.setState({
addLoading: false,
addError: err
addError: err,
});
});
}
@@ -184,7 +186,7 @@ class AddUserContent extends React.Component<
accessKey,
secretKey,
selectedGroups,
enabled
enabled,
} = this.state;
return (
@@ -204,68 +206,67 @@ class AddUserContent extends React.Component<
}}
>
<Grid container>
{addError !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
</Grid>
)}
<Grid item xs={12} className={classes.formScrollable}>
{addError !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
</Grid>
)}
<InputBoxWrapper
id="accesskey-input"
name="accesskey-input"
label="Access Key"
value={accessKey}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ accessKey: e.target.value });
}}
disabled={selectedUser !== null}
/>
{selectedUser !== null ? (
<RadioGroupSelector
currentSelection={enabled}
id="user-status"
name="user-status"
label="Status"
onChange={e => {
this.setState({ enabled: e.target.value });
}}
selectorOptions={[
{ label: "Enabled", value: "enabled" },
{ label: "Disabled", value: "disabled" }
]}
/>
) : (
<InputBoxWrapper
id="standard-multiline-static"
name="standard-multiline-static"
label="Secret Key"
type="password"
value={secretKey}
id="accesskey-input"
name="accesskey-input"
label="Access Key"
value={accessKey}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ secretKey: e.target.value });
this.setState({ accessKey: e.target.value });
}}
autoComplete="current-password"
disabled={selectedUser !== null}
/>
)}
<Grid item xs={12}>
<GroupsSelectors
selectedGroups={selectedGroups}
setSelectedGroups={(elements: string[]) => {
this.setState({
selectedGroups: elements
});
}}
/>
</Grid>
<Grid item xs={12}>
<br />
{selectedUser !== null ? (
<RadioGroupSelector
currentSelection={enabled}
id="user-status"
name="user-status"
label="Status"
onChange={(e) => {
this.setState({ enabled: e.target.value });
}}
selectorOptions={[
{ label: "Enabled", value: "enabled" },
{ label: "Disabled", value: "disabled" },
]}
/>
) : (
<InputBoxWrapper
id="standard-multiline-static"
name="standard-multiline-static"
label="Secret Key"
type="password"
value={secretKey}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ secretKey: e.target.value });
}}
autoComplete="current-password"
/>
)}
<Grid item xs={12}>
<GroupsSelectors
selectedGroups={selectedGroups}
setSelectedGroups={(elements: string[]) => {
this.setState({
selectedGroups: elements,
});
}}
/>
</Grid>
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>
<Button

View File

@@ -0,0 +1,259 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 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/>.
import React, { useEffect, useState } from "react";
import {
Button,
Grid,
Typography,
TextField
} from "@material-ui/core";
import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket";
import { AppState } from "../../../store";
import { connect } from "react-redux";
import { watchMessageReceived, watchResetMessages } from "./actions";
import { EventInfo, BucketList, Bucket } from "./types";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { niceBytes, timeFromDate } from "../../../common/utils";
import { wsProtocol } from "../../../utils/wsUtils";
import api from "../../../common/api";
import {
FormControl,
MenuItem,
Select,
} from "@material-ui/core";
const styles = (theme: Theme) =>
createStyles({
watchList: {
background: "white",
maxHeight: "400px",
overflow: "auto",
"& ul": {
margin: "4px",
padding: "0px"
},
"& ul li": {
listStyle: "none",
margin: "0px",
padding: "0px",
borderBottom: "1px solid #dedede"
}
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10,
}
},
inputField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
marginLeft: 10,
boxShadow: "0px 3px 6px #00000012"
},
fieldContainer: {
background: "#FFFFFF",
padding: 0,
borderRadius: 5,
marginLeft: 10,
textAlign: "left",
minWidth: "206px",
boxShadow: "0px 3px 6px #00000012"
}
});
interface IWatch {
classes: any;
watchMessageReceived: typeof watchMessageReceived;
watchResetMessages: typeof watchResetMessages;
messages: EventInfo[];
}
const Watch = ({
classes,
watchMessageReceived,
watchResetMessages,
messages
}: IWatch) => {
const [start, setStart] = useState(false);
const [bucketName, setBucketName] = useState("Select Bucket");
const [prefix, setPrefix] = useState("");
const [suffix, setSuffix] = useState("");
const [bucketList, setBucketList] = useState<Bucket[]>([]);
const fetchBucketList = () => {
api
.invoke("GET", `/api/v1/buckets`)
.then((res: BucketList) => {
let buckets: Bucket[] = [];
if (res.buckets !== null) {
buckets = res.buckets;
}
setBucketList(buckets);
})
.catch((err: any) => {
console.log(err);
});
}
useEffect(() => {
fetchBucketList();
}, []);
useEffect(() => {
watchResetMessages();
// begin watch if bucketName in bucketList and start pressed
if (start && bucketList.some(bucket => bucket.name === bucketName)) {
const url = new URL(window.location.toString());
const isDev = process.env.NODE_ENV === "development";
const port = isDev ? "9090" : url.port;
const wsProt = wsProtocol(url.protocol);
const c = new W3CWebSocket(`${wsProt}://${url.hostname}:${port}/ws/watch/${bucketName}?prefix=${prefix}&suffix=${suffix}`);
let interval: any | null = null;
if (c !== null) {
c.onopen = () => {
console.log("WebSocket Client Connected");
c.send("ok");
interval = setInterval(() => {
c.send("ok");
}, 10 * 1000);
};
c.onmessage = (message: IMessageEvent) => {
let m: EventInfo = JSON.parse(message.data.toString());
m.Time = new Date(m.Time.toString());
m.key = Math.random();
watchMessageReceived(m);
};
c.onclose = () => {
clearInterval(interval);
console.log("connection closed by server");
};
return () => {
// close websocket on useEffect cleanup
c.close(1000);
clearInterval(interval);
console.log("closing websockets");
};
}
} else {
// reset start status
setStart(false);
}
}, [watchMessageReceived, start]);
const bucketNames = bucketList.map(bucketName => ({
label: bucketName.name,
value: bucketName.name
}));
return (
<React.Fragment>
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">Watch</Typography>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12} className={classes.actionsTray}>
<FormControl variant="outlined">
<Select
id="bucket-name"
name="bucket-name"
value={bucketName}
onChange={(e) => { setBucketName(e.target.value as string) }}
className={classes.fieldContainer}
disabled={start}
>
<MenuItem
value={bucketName}
key={`select-bucket-name-default`}
disabled={true}
>
Select Bucket
</MenuItem>
{bucketNames.map(option => (
<MenuItem
value={option.value}
key={`select-bucket-name-${option.label}`}
>
{option.label}
</MenuItem>
))}
</Select>
</FormControl>
<TextField
placeholder="Prefix"
className={classes.inputField}
id="prefix-resource"
label=""
disabled={start}
InputProps={{
disableUnderline: true,
}}
onChange={(e) => { setPrefix(e.target.value) }}
/>
<TextField
placeholder="Suffix"
className={classes.inputField}
id="suffix-resource"
label=""
disabled={start}
InputProps={{
disableUnderline: true,
}}
onChange={(e) => { setSuffix(e.target.value) }}
/>
<Button
type="submit"
variant="contained"
color="primary"
disabled={start}
onClick={() => setStart(true)}
>
Start
</Button>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
</Grid>
<div className={classes.watchList}>
<ul>
{messages.map(m => {
return (
<li key={m.key}>
{timeFromDate(m.Time)} - {niceBytes(m.Size + "")} - {m.Type} - {m.Path}
</li>
);
})}
</ul>
</div>
</React.Fragment>
);
};
const mapState = (state: AppState) => ({
messages: state.watch.messages
});
const connector = connect(mapState, {
watchMessageReceived: watchMessageReceived,
watchResetMessages: watchResetMessages
});
export default connector(withStyles(styles)(Watch));

View File

@@ -0,0 +1,47 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 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/>.
import { EventInfo } from "./types";
export const WATCH_MESSAGE_RECEIVED = "WATCH_MESSAGE_RECEIVED";
export const WATCH_RESET_MESSAGES = "WATCH_RESET_MESSAGES";
interface WatchMessageReceivedAction {
type: typeof WATCH_MESSAGE_RECEIVED;
message: EventInfo;
}
interface WatchResetMessagesAction {
type: typeof WATCH_RESET_MESSAGES;
}
export type WatchActionTypes =
| WatchMessageReceivedAction
| WatchResetMessagesAction;
export function watchMessageReceived(message: EventInfo) {
return {
type: WATCH_MESSAGE_RECEIVED,
message: message
};
}
export function watchResetMessages() {
return {
type: WATCH_RESET_MESSAGES
};
}

View File

@@ -0,0 +1,50 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 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/>.
import {
WATCH_MESSAGE_RECEIVED,
WATCH_RESET_MESSAGES,
WatchActionTypes
} from "./actions";
import { EventInfo } from "./types";
export interface WatchState {
messages: EventInfo[];
}
const initialState: WatchState = {
messages: []
};
export function watchReducer(
state = initialState,
action: WatchActionTypes
): WatchState {
switch (action.type) {
case WATCH_MESSAGE_RECEIVED:
return {
...state,
messages: [...state.messages, action.message]
};
case WATCH_RESET_MESSAGES:
return {
...state,
messages: []
};
default:
return state;
}
}

View File

@@ -0,0 +1,36 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 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/>.
export interface EventInfo {
Time: Date;
Size: number;
UserMetadata: Map<string, string>;
Path: string;
Type: string;
Host: string;
Port: string;
UserAgent: string;
key: number;
}
export interface Bucket {
name: string;
}
export interface BucketList {
buckets: Bucket[];
total: number;
}

View File

@@ -19,11 +19,13 @@ import thunk from "redux-thunk";
import { systemReducer } from "./reducer";
import { traceReducer } from "./screens/Console/Trace/reducers";
import { logReducer } from "./screens/Console/Logs/reducers";
import { watchReducer } from "./screens/Console/Watch/reducers";
const globalReducer = combineReducers({
system: systemReducer,
trace: traceReducer,
logs: logReducer
logs: logReducer,
watch: watchReducer,
});
declare global {

View File

@@ -15,7 +15,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
export const wsProtocol = (protocol: string): string => {
let wsProtocol = "ws";
if (protocol == "https:") {
if (protocol === "https:") {
wsProtocol = "wss";
}
return wsProtocol;

View File

@@ -39,7 +39,6 @@ func TestAdminConsoleLog(t *testing.T) {
assert := assert.New(t)
adminClient := adminClientMock{}
mockWSConn := mockConn{}
wsClientMock := wsClientMock{madmin: adminClient}
function := "startConsoleLog()"
testReceiver := make(chan madmin.LogInfo, 5)
@@ -83,7 +82,7 @@ func TestAdminConsoleLog(t *testing.T) {
writesCount++
return nil
}
if err := startConsoleLog(mockWSConn, wsClientMock.madmin); err != nil {
if err := startConsoleLog(mockWSConn, adminClient); err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
}
// check that the TestReceiver got the same number of data from Console.
@@ -95,7 +94,7 @@ func TestAdminConsoleLog(t *testing.T) {
connWriteMessageMock = func(messageType int, data []byte) error {
return fmt.Errorf("error on write")
}
if err := startConsoleLog(mockWSConn, wsClientMock.madmin); assert.Error(err) {
if err := startConsoleLog(mockWSConn, adminClient); assert.Error(err) {
assert.Equal("error on write", err.Error())
}
@@ -107,7 +106,7 @@ func TestAdminConsoleLog(t *testing.T) {
connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseAbnormalClosure, Text: ""}
}
if err := startConsoleLog(mockWSConn, wsClientMock.madmin); assert.Error(err) {
if err := startConsoleLog(mockWSConn, adminClient); assert.Error(err) {
assert.Equal("websocket: close 1006 (abnormal closure)", err.Error())
}
@@ -116,7 +115,7 @@ func TestAdminConsoleLog(t *testing.T) {
connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseNormalClosure, Text: ""}
}
if err := startConsoleLog(mockWSConn, wsClientMock.madmin); err != nil {
if err := startConsoleLog(mockWSConn, adminClient); err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
}
@@ -125,7 +124,7 @@ func TestAdminConsoleLog(t *testing.T) {
connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseGoingAway, Text: ""}
}
if err := startConsoleLog(mockWSConn, wsClientMock.madmin); err != nil {
if err := startConsoleLog(mockWSConn, adminClient); err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
}
@@ -134,7 +133,7 @@ func TestAdminConsoleLog(t *testing.T) {
connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, fmt.Errorf("error on read")
}
if err := startConsoleLog(mockWSConn, wsClientMock.madmin); assert.Error(err) {
if err := startConsoleLog(mockWSConn, adminClient); assert.Error(err) {
assert.Equal("error on read", err.Error())
}
@@ -162,7 +161,7 @@ func TestAdminConsoleLog(t *testing.T) {
connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, nil
}
if err := startConsoleLog(mockWSConn, wsClientMock.madmin); assert.Error(err) {
if err := startConsoleLog(mockWSConn, adminClient); assert.Error(err) {
assert.Equal("error on Console", err.Error())
}
}

View File

@@ -40,7 +40,6 @@ func TestAdminTrace(t *testing.T) {
assert := assert.New(t)
adminClient := adminClientMock{}
mockWSConn := mockConn{}
wsClientMock := wsClientMock{madmin: adminClient}
function := "startTraceInfo()"
testReceiver := make(chan shortTraceMsg, 5)
@@ -84,7 +83,7 @@ func TestAdminTrace(t *testing.T) {
writesCount++
return nil
}
if err := startTraceInfo(mockWSConn, wsClientMock.madmin); err != nil {
if err := startTraceInfo(mockWSConn, adminClient); err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
}
// check that the TestReceiver got the same number of data from trace.
@@ -96,7 +95,7 @@ func TestAdminTrace(t *testing.T) {
connWriteMessageMock = func(messageType int, data []byte) error {
return fmt.Errorf("error on write")
}
if err := startTraceInfo(mockWSConn, wsClientMock.madmin); assert.Error(err) {
if err := startTraceInfo(mockWSConn, adminClient); assert.Error(err) {
assert.Equal("error on write", err.Error())
}
@@ -108,7 +107,7 @@ func TestAdminTrace(t *testing.T) {
connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseAbnormalClosure, Text: ""}
}
if err := startTraceInfo(mockWSConn, wsClientMock.madmin); assert.Error(err) {
if err := startTraceInfo(mockWSConn, adminClient); assert.Error(err) {
assert.Equal("websocket: close 1006 (abnormal closure)", err.Error())
}
@@ -117,7 +116,7 @@ func TestAdminTrace(t *testing.T) {
connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseNormalClosure, Text: ""}
}
if err := startTraceInfo(mockWSConn, wsClientMock.madmin); err != nil {
if err := startTraceInfo(mockWSConn, adminClient); err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
}
@@ -126,7 +125,7 @@ func TestAdminTrace(t *testing.T) {
connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseGoingAway, Text: ""}
}
if err := startTraceInfo(mockWSConn, wsClientMock.madmin); err != nil {
if err := startTraceInfo(mockWSConn, adminClient); err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
}
@@ -135,7 +134,7 @@ func TestAdminTrace(t *testing.T) {
connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, fmt.Errorf("error on read")
}
if err := startTraceInfo(mockWSConn, wsClientMock.madmin); assert.Error(err) {
if err := startTraceInfo(mockWSConn, adminClient); assert.Error(err) {
assert.Equal("error on read", err.Error())
}
@@ -163,7 +162,7 @@ func TestAdminTrace(t *testing.T) {
connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, nil
}
if err := startTraceInfo(mockWSConn, wsClientMock.madmin); assert.Error(err) {
if err := startTraceInfo(mockWSConn, adminClient); assert.Error(err) {
assert.Equal("error on trace", err.Error())
}
}

View File

@@ -44,7 +44,9 @@ func NewAdminClient(url, accessKey, secretKey string) (*madmin.AdminClient, *pro
AppName: appName,
AppVersion: McsVersion,
AppComments: []string{appName, runtime.GOOS, runtime.GOARCH},
Insecure: false,
})
s3Client.SetCustomTransport(STSClient.Transport)
if err != nil {
return nil, err.Trace(url)
}
@@ -240,13 +242,15 @@ func newMAdminClient(jwt string) (*madmin.AdminClient, error) {
// newAdminFromClaims creates a minio admin from Decrypted claims using Assume role credentials
func newAdminFromClaims(claims *auth.DecryptedClaims) (*madmin.AdminClient, error) {
tlsEnabled := getMinIOEndpointIsSecure()
adminClient, err := madmin.NewWithOptions(getMinIOEndpoint(), &madmin.Options{
Creds: credentials.NewStaticV4(claims.AccessKeyID, claims.SecretAccessKey, claims.SessionToken),
Secure: getMinIOEndpointIsSecure(),
Secure: tlsEnabled,
})
if err != nil {
return nil, err
}
adminClient.SetCustomTransport(STSClient.Transport)
return adminClient, nil
}

View File

@@ -19,11 +19,15 @@ package restapi
import (
"context"
"fmt"
"strings"
"errors"
mc "github.com/minio/mc/cmd"
"github.com/minio/mc/pkg/probe"
"github.com/minio/mcs/pkg/auth"
xjwt "github.com/minio/mcs/pkg/auth/jwt"
"github.com/minio/mcs/pkg/auth/ldap"
"github.com/minio/minio-go/v6"
"github.com/minio/minio-go/v6/pkg/credentials"
)
@@ -90,6 +94,7 @@ func (c minioClient) getBucketPolicy(bucketName string) (string, error) {
type MCS3Client interface {
addNotificationConfig(arn string, events []string, prefix, suffix string, ignoreExisting bool) *probe.Error
removeNotificationConfig(arn string, event string, prefix string, suffix string) *probe.Error
watch(options mc.WatchOptions) (*mc.WatchObject, *probe.Error)
}
// Interface implementation
@@ -110,6 +115,10 @@ func (c mcS3Client) removeNotificationConfig(arn string, event string, prefix st
return c.client.RemoveNotificationConfig(arn, event, prefix, suffix)
}
func (c mcS3Client) watch(options mc.WatchOptions) (*mc.WatchObject, *probe.Error) {
return c.client.Watch(options)
}
// MCSCredentials interface with all functions to be implemented
// by mock when testing, it should include all needed minioCredentials.Credentials api calls
// that are used within this project.
@@ -133,13 +142,62 @@ func (c mcsCredentials) Expire() {
c.minioCredentials.Expire()
}
// mcsSTSAssumeRole it's a STSAssumeRole wrapper, in general
// there's no need to use this struct anywhere else in the project, it's only required
// for passing a custom *http.Client to *credentials.STSAssumeRole
type mcsSTSAssumeRole struct {
stsAssumeRole *credentials.STSAssumeRole
}
func (s mcsSTSAssumeRole) Retrieve() (credentials.Value, error) {
return s.stsAssumeRole.Retrieve()
}
func (s mcsSTSAssumeRole) IsExpired() bool {
return s.stsAssumeRole.IsExpired()
}
// STSClient contains http.client configuration need it by STSAssumeRole
var STSClient = PrepareSTSClient()
func newMcsCredentials(accessKey, secretKey, location string) (*credentials.Credentials, error) {
return credentials.NewSTSAssumeRole(getMinIOServer(), credentials.STSAssumeRoleOptions{
AccessKey: accessKey,
SecretKey: secretKey,
Location: location,
DurationSeconds: xjwt.GetMcsSTSAndJWTDurationInSeconds(),
})
mcsEndpoint := getMinIOServer()
if mcsEndpoint == "" {
return nil, errors.New("STS endpoint cannot be empty")
}
if accessKey == "" || secretKey == "" {
return nil, errors.New("AssumeRole credentials access/secretkey is mandatory")
}
// Future authentication methods can be added under this switch statement
switch {
// LDAP authentication for MCS
case ldap.GetLDAPEnabled():
{
creds, err := auth.GetMcsCredentialsFromLDAP(mcsEndpoint, accessKey, secretKey)
if err != nil {
return nil, err
}
return creds, nil
}
// default authentication for MCS is via STS (Security Token Service) against MinIO
default:
{
opts := credentials.STSAssumeRoleOptions{
AccessKey: accessKey,
SecretKey: secretKey,
Location: location,
DurationSeconds: xjwt.GetMcsSTSAndJWTDurationInSeconds(),
}
stsAssumeRole := &credentials.STSAssumeRole{
Client: STSClient,
STSEndpoint: mcsEndpoint,
Options: opts,
}
mcsSTSWrapper := mcsSTSAssumeRole{stsAssumeRole: stsAssumeRole}
return credentials.New(mcsSTSWrapper), nil
}
}
}
// getMcsCredentialsFromJWT returns the *minioCredentials.Credentials associated to the
@@ -160,30 +218,35 @@ func newMinioClient(jwt string) (*minio.Client, error) {
if err != nil {
return nil, err
}
adminClient, err := minio.NewWithOptions(getMinIOEndpoint(), &minio.Options{
minioClient, err := minio.NewWithOptions(getMinIOEndpoint(), &minio.Options{
Creds: creds,
Secure: getMinIOEndpointIsSecure(),
})
if err != nil {
return nil, err
}
return adminClient, nil
minioClient.SetCustomTransport(STSClient.Transport)
return minioClient, nil
}
// newS3BucketClient creates a new mc S3Client to talk to the server based on a bucket
func newS3BucketClient(bucketName *string) (*mc.S3Client, error) {
func newS3BucketClient(jwt string, bucketName string) (*mc.S3Client, error) {
endpoint := getMinIOServer()
accessKeyID := getAccessKey()
secretAccessKey := getSecretKey()
useSSL := getMinIOEndpointIsSecure()
if bucketName != nil {
endpoint += fmt.Sprintf("/%s", *bucketName)
}
s3Config := newS3Config(endpoint, accessKeyID, secretAccessKey, !useSSL)
client, err := mc.S3New(s3Config)
claims, err := auth.JWTAuthenticate(jwt)
if err != nil {
return nil, err.Cause
return nil, err
}
if strings.TrimSpace(bucketName) != "" {
endpoint += fmt.Sprintf("/%s", bucketName)
}
s3Config := newS3Config(endpoint, claims.AccessKeyID, claims.SecretAccessKey, claims.SessionToken, !useSSL)
client, pErr := mc.S3New(s3Config)
if pErr != nil {
return nil, pErr.Cause
}
s3Client, ok := client.(*mc.S3Client)
if !ok {
@@ -195,7 +258,7 @@ func newS3BucketClient(bucketName *string) (*mc.S3Client, error) {
// newS3Config simply creates a new Config struct using the passed
// parameters.
func newS3Config(endpoint, accessKey, secretKey string, isSecure bool) *mc.Config {
func newS3Config(endpoint, accessKey, secretKey, sessionToken string, isSecure bool) *mc.Config {
// We have a valid alias and hostConfig. We populate the
// minioCredentials from the match found in the config file.
s3Config := new(mc.Config)
@@ -209,6 +272,7 @@ func newS3Config(endpoint, accessKey, secretKey string, isSecure bool) *mc.Confi
s3Config.HostURL = endpoint
s3Config.AccessKey = accessKey
s3Config.SecretKey = secretKey
s3Config.SessionToken = sessionToken
s3Config.Signature = "S3v4"
return s3Config
}

View File

@@ -48,7 +48,17 @@ func getSecretKey() string {
}
func getMinIOServer() string {
return env.Get(McsMinIOServer, "http://localhost:9000")
return strings.TrimSpace(env.Get(McsMinIOServer, "http://localhost:9000"))
}
// If MCS_MINIO_SERVER_TLS_ROOT_CAS is true mcs will load a list of certificates into the
// http.client rootCAs store, this is useful for testing or when working with self-signed certificates
func getMinioServerTLSRootCAs() []string {
caCertFileNames := strings.TrimSpace(env.Get(McsMinIOServerTLSRootCAs, ""))
if caCertFileNames == "" {
return []string{}
}
return strings.Split(caCertFileNames, ",")
}
func getMinIOEndpoint() string {
@@ -67,7 +77,7 @@ func getMinIOEndpointIsSecure() bool {
if strings.Contains(server, "://") {
parts := strings.Split(server, "://")
if len(parts) > 1 {
if parts[1] == "https" {
if parts[0] == "https" {
return true
}
}

View File

@@ -18,15 +18,16 @@ package restapi
const (
// consts for common configuration
McsVersion = `0.1.0`
McsAccessKey = "MCS_ACCESS_KEY"
McsSecretKey = "MCS_SECRET_KEY"
McsMinIOServer = "MCS_MINIO_SERVER"
McsProductionMode = "MCS_PRODUCTION_MODE"
McsHostname = "MCS_HOSTNAME"
McsPort = "MCS_PORT"
McsTLSHostname = "MCS_TLS_HOSTNAME"
McsTLSPort = "MCS_TLS_PORT"
McsVersion = `0.1.0`
McsAccessKey = "MCS_ACCESS_KEY"
McsSecretKey = "MCS_SECRET_KEY"
McsMinIOServer = "MCS_MINIO_SERVER"
McsMinIOServerTLSRootCAs = "MCS_MINIO_SERVER_TLS_ROOT_CAS"
McsProductionMode = "MCS_PRODUCTION_MODE"
McsHostname = "MCS_HOSTNAME"
McsPort = "MCS_PORT"
McsTLSHostname = "MCS_TLS_HOSTNAME"
McsTLSPort = "MCS_TLS_PORT"
// consts for Secure middleware
McsSecureAllowedHosts = "MCS_SECURE_ALLOWED_HOSTS"

95
restapi/tls.go Normal file
View File

@@ -0,0 +1,95 @@
// This file is part of MinIO Orchestrator
// Copyright (c) 2020 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 restapi
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"net"
"net/http"
"time"
)
var (
certDontExists = "File certificate doesn't exists: %s"
)
func prepareSTSClientTransport() *http.Transport {
// This takes github.com/minio/minio/pkg/madmin/transport.go as an example
//
// DefaultTransport - this default transport is similar to
// http.DefaultTransport but with additional param DisableCompression
// is set to true to avoid decompressing content with 'gzip' encoding.
DefaultTransport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 15 * time.Second,
}).DialContext,
MaxIdleConns: 1024,
MaxIdleConnsPerHost: 1024,
ResponseHeaderTimeout: 60 * time.Second,
IdleConnTimeout: 60 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
DisableCompression: true,
}
// If Minio instance is running with TLS enabled and it's using a self-signed certificate
// or a certificate issued by a custom certificate authority we prepare a new custom *http.Transport
if getMinIOEndpointIsSecure() {
caCertFileNames := getMinioServerTLSRootCAs()
tlsConfig := &tls.Config{
// Can't use SSLv3 because of POODLE and BEAST
// Can't use TLSv1.0 because of POODLE and BEAST using CBC cipher
// Can't use TLSv1.1 because of RC4 cipher usage
MinVersion: tls.VersionTLS12,
}
// If root CAs are configured we save them to the http.Client RootCAs store
if len(caCertFileNames) > 0 {
certs := x509.NewCertPool()
for _, caCert := range caCertFileNames {
// Validate certificate exists
if FileExists(caCert) {
pemData, err := ioutil.ReadFile(caCert)
if err != nil {
// if there was an error reading pem file stop mcs
panic(err)
}
certs.AppendCertsFromPEM(pemData)
} else {
// if provided cert filename doesn't exists stop mcs
panic(fmt.Sprintf(certDontExists, caCert))
}
}
tlsConfig.RootCAs = certs
}
DefaultTransport.TLSClientConfig = tlsConfig
}
return DefaultTransport
}
// PrepareSTSClient returns an http.Client with custom configurations need it by *credentials.STSAssumeRole
// custom configurations include skipVerification flag, and root CA certificates
func PrepareSTSClient() *http.Client {
transport := prepareSTSClientTransport()
// Return http client with default configuration
return &http.Client{
Transport: transport,
}
}

View File

@@ -40,14 +40,16 @@ func registerBucketEventsHandlers(api *operations.McsAPI) {
})
// create bucket event
api.UserAPICreateBucketEventHandler = user_api.CreateBucketEventHandlerFunc(func(params user_api.CreateBucketEventParams, principal *models.Principal) middleware.Responder {
if err := getCreateBucketEventsResponse(params.BucketName, params.Body); err != nil {
sessionID := string(*principal)
if err := getCreateBucketEventsResponse(sessionID, params.BucketName, params.Body); err != nil {
return user_api.NewCreateBucketEventDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
return user_api.NewCreateBucketEventCreated()
})
// delete bucket event
api.UserAPIDeleteBucketEventHandler = user_api.DeleteBucketEventHandlerFunc(func(params user_api.DeleteBucketEventParams, principal *models.Principal) middleware.Responder {
if err := getDeleteBucketEventsResponse(params.BucketName, params.Arn, params.Body.Events, params.Body.Prefix, params.Body.Suffix); err != nil {
sessionID := string(*principal)
if err := getDeleteBucketEventsResponse(sessionID, params.BucketName, params.Arn, params.Body.Events, params.Body.Prefix, params.Body.Suffix); err != nil {
return user_api.NewDeleteBucketEventDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
return user_api.NewDeleteBucketEventNoContent()
@@ -178,8 +180,8 @@ func createBucketEvent(client MCS3Client, arn string, notificationEvents []model
}
// getCreateBucketEventsResponse calls createBucketEvent to add a bucket event notification
func getCreateBucketEventsResponse(bucketName string, eventReq *models.BucketEventRequest) error {
s3Client, err := newS3BucketClient(swag.String(bucketName))
func getCreateBucketEventsResponse(sessionID, bucketName string, eventReq *models.BucketEventRequest) error {
s3Client, err := newS3BucketClient(sessionID, bucketName)
if err != nil {
log.Println("error creating S3Client:", err)
return err
@@ -214,8 +216,8 @@ func joinNotificationEvents(events []models.NotificationEventType) string {
}
// getDeleteBucketEventsResponse calls deleteBucketEventNotification() to delete a bucket event notification
func getDeleteBucketEventsResponse(bucketName string, arn string, events []models.NotificationEventType, prefix, suffix *string) error {
s3Client, err := newS3BucketClient(swag.String(bucketName))
func getDeleteBucketEventsResponse(sessionID, bucketName string, arn string, events []models.NotificationEventType, prefix, suffix *string) error {
s3Client, err := newS3BucketClient(sessionID, bucketName)
if err != nil {
log.Println("error creating S3Client:", err)
return err

View File

@@ -32,7 +32,8 @@ import (
)
var (
errorGeneric = errors.New("an error occurred, please try again")
errorGeneric = errors.New("an error occurred, please try again")
errInvalidCredentials = errors.New("invalid Credentials")
)
func registerLoginHandlers(api *operations.McsAPI) {
@@ -48,48 +49,48 @@ func registerLoginHandlers(api *operations.McsAPI) {
api.UserAPILoginHandler = user_api.LoginHandlerFunc(func(params user_api.LoginParams) middleware.Responder {
loginResponse, err := getLoginResponse(params.Body)
if err != nil {
return user_api.NewLoginDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
return user_api.NewLoginDefault(401).WithPayload(&models.Error{Code: 401, Message: swag.String(err.Error())})
}
return user_api.NewLoginCreated().WithPayload(loginResponse)
})
api.UserAPILoginOauth2AuthHandler = user_api.LoginOauth2AuthHandlerFunc(func(params user_api.LoginOauth2AuthParams) middleware.Responder {
loginResponse, err := getLoginOauth2AuthResponse(params.Body)
if err != nil {
return user_api.NewLoginOauth2AuthDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
return user_api.NewLoginOauth2AuthDefault(401).WithPayload(&models.Error{Code: 401, Message: swag.String(err.Error())})
}
return user_api.NewLoginOauth2AuthCreated().WithPayload(loginResponse)
})
}
var errInvalidCredentials = errors.New("invalid minioCredentials")
// login performs a check of minioCredentials against MinIO
func login(credentials MCSCredentials) (*string, error) {
// try to obtain minioCredentials,
tokens, err := credentials.Get()
if err != nil {
log.Println("error authenticating user", err)
return nil, errInvalidCredentials
}
// if we made it here, the minioCredentials work, generate a jwt with claims
jwt, err := auth.NewJWTWithClaimsForClient(&tokens, getMinIOServer())
if err != nil {
log.Println("error authenticating user", err)
return nil, errInvalidCredentials
}
return &jwt, nil
}
func getConfiguredRegion(client MinioAdmin) string {
func getConfiguredRegionForLogin(client MinioAdmin) (string, error) {
location := ""
configuration, err := getConfig(client, "region")
if err != nil {
log.Println("error obtaining MinIO region:", err)
return location
return location, errorGeneric
}
// region is an array of 1 element
if len(configuration) > 0 {
location = configuration[0].Value
}
return location
return location, nil
}
// getLoginResponse performs login() and serializes it to the handler's output
@@ -102,16 +103,18 @@ func getLoginResponse(lr *models.LoginRequest) (*models.LoginResponse, error) {
adminClient := adminClient{client: mAdmin}
// obtain the configured MinIO region
// need it for user authentication
location := getConfiguredRegion(adminClient)
location, err := getConfiguredRegionForLogin(adminClient)
if err != nil {
return nil, err
}
creds, err := newMcsCredentials(*lr.AccessKey, *lr.SecretKey, location)
if err != nil {
log.Println("error login:", err)
return nil, err
return nil, errInvalidCredentials
}
credentials := mcsCredentials{minioCredentials: creds}
sessionID, err := login(credentials)
if err != nil {
log.Println("error login:", err)
return nil, err
}
// serialize output
@@ -131,7 +134,8 @@ func getLoginDetailsResponse() (*models.LoginDetails, error) {
// initialize new oauth2 client
oauth2Client, err := oauth2.NewOauth2ProviderClient(ctx, nil)
if err != nil {
return nil, err
log.Println("error getting new oauth2 provider client", err)
return nil, errorGeneric
}
// Validate user against IDP
identityProvider := &auth.IdentityProvider{Client: oauth2Client}
@@ -147,7 +151,8 @@ func getLoginDetailsResponse() (*models.LoginDetails, error) {
func loginOauth2Auth(ctx context.Context, provider *auth.IdentityProvider, code, state string) (*oauth2.User, error) {
userIdentity, err := provider.VerifyIdentity(ctx, code, state)
if err != nil {
return nil, err
log.Println("error validating user identity against idp:", err)
return nil, errorGeneric
}
return userIdentity, nil
}
@@ -166,8 +171,7 @@ func getLoginOauth2AuthResponse(lr *models.LoginOauth2AuthRequest) (*models.Logi
// Validate user against IDP
identity, err := loginOauth2Auth(ctx, identityProvider, *lr.Code, *lr.State)
if err != nil {
log.Println("error validating user identity against idp:", err)
return nil, errorGeneric
return nil, err
}
mAdmin, err := newSuperMAdminClient()
if err != nil {
@@ -179,7 +183,10 @@ func getLoginOauth2AuthResponse(lr *models.LoginOauth2AuthRequest) (*models.Logi
secretKey := utils.RandomCharString(32)
// obtain the configured MinIO region
// need it for user authentication
location := getConfiguredRegion(adminClient)
location, err := getConfiguredRegionForLogin(adminClient)
if err != nil {
return nil, err
}
// create user in MinIO
if _, err := addUser(ctx, adminClient, &accessKey, &secretKey, []string{}); err != nil {
log.Println("error adding user:", err)
@@ -207,8 +214,7 @@ func getLoginOauth2AuthResponse(lr *models.LoginOauth2AuthRequest) (*models.Logi
credentials := mcsCredentials{minioCredentials: creds}
jwt, err := login(credentials)
if err != nil {
log.Println("error login:", err)
return nil, errorGeneric
return nil, err
}
// serialize output
loginResponse := &models.LoginResponse{

View File

@@ -100,7 +100,7 @@ func TestLoginOauth2Auth(t *testing.T) {
return nil, errors.New("error")
}
if _, err := loginOauth2Auth(ctx, identityProvider, mockCode, mockState); funcAssert.Error(err) {
funcAssert.Equal("error", err.Error())
funcAssert.Equal("an error occurred, please try again", err.Error())
}
}
@@ -189,8 +189,8 @@ func Test_getConfiguredRegion(t *testing.T) {
for _, tt := range tests {
tt.mock()
t.Run(tt.name, func(t *testing.T) {
if got := getConfiguredRegion(tt.args.client); got != tt.want {
t.Errorf("getConfiguredRegion() = %v, want %v", got, tt.want)
if got, _ := getConfiguredRegionForLogin(tt.args.client); got != tt.want {
t.Errorf("getConfiguredRegionForLogin() = %v, want %v", got, tt.want)
}
})
}

165
restapi/user_watch.go Normal file
View File

@@ -0,0 +1,165 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 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 restapi
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"regexp"
"strings"
"sync"
"github.com/gorilla/websocket"
mc "github.com/minio/mc/cmd"
)
type watchOptions struct {
BucketName string
mc.WatchOptions
}
// startWatch starts by setting a websocket reader that
// will check for a heartbeat.
//
// A WaitGroup is used to handle goroutines and to ensure
// all finish in the proper order. If any, sendWatchInfo()
// or wsReadCheck() returns, watch should end.
func startWatch(conn WSConn, client MCS3Client, options watchOptions) (mError error) {
// a WaitGroup waits for a collection of goroutines to finish
wg := sync.WaitGroup{}
// a cancel context is needed to end all goroutines used
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Set number of goroutines to wait. wg.Wait()
// waits until counter is zero (all are done)
wg.Add(3)
// start go routine for reading websocket heartbeat
readErr := wsReadCheck(ctx, &wg, conn)
// send Stream of watch events to the ws c.connection
ch := sendWatchInfo(ctx, &wg, conn, client, options)
// If wsReadCheck returns it means that it is not possible to check
// ws heartbeat anymore so we stop from doing Watch, cancel context
// for all goroutines.
go func(wg *sync.WaitGroup) {
defer wg.Done()
if err := <-readErr; err != nil {
log.Println("error on wsReadCheck:", err)
mError = err
}
// cancel context for all goroutines.
cancel()
}(&wg)
if err := <-ch; err != nil {
mError = err
}
// if ch closes for any reason,
// cancel context for all goroutines
cancel()
// wait all goroutines to finish
wg.Wait()
return mError
}
// sendWatchInfo sends stream of Watch Event to the ws connection
func sendWatchInfo(ctx context.Context, wg *sync.WaitGroup, conn WSConn, wsc MCS3Client, options watchOptions) <-chan error {
// decrements the WaitGroup counter
// by one when the function returns
defer wg.Done()
ch := make(chan error)
go func(ch chan<- error) {
defer close(ch)
wo, pErr := wsc.watch(options.WatchOptions)
if pErr != nil {
fmt.Println("error initializing watch:", pErr.Cause)
ch <- pErr.Cause
return
}
for {
select {
case <-ctx.Done():
close(wo.DoneChan)
return
case events, ok := <-wo.Events():
// zero value returned because the channel is closed and empty
if !ok {
return
}
for _, event := range events {
// Serialize message to be sent
bytes, err := json.Marshal(event)
if err != nil {
fmt.Println("error on json.Marshal:", err)
ch <- err
return
}
// Send Message through websocket connection
err = conn.writeMessage(websocket.TextMessage, bytes)
if err != nil {
log.Println("error writeMessage:", err)
ch <- err
return
}
}
case pErr, ok := <-wo.Errors():
// zero value returned because the channel is closed and empty
if !ok {
return
}
if pErr != nil {
log.Println("error on watch:", pErr.Cause)
ch <- pErr.Cause
return
}
}
}
}(ch)
return ch
}
// getOptionsFromReq gets bucket name, events, prefix, suffix from a websocket
// watch path if defined.
// path come as : `/watch/bucket1` and query params come on request form
func getOptionsFromReq(req *http.Request) watchOptions {
wOptions := watchOptions{}
// Default Events if not defined
wOptions.Events = []string{"put", "get", "delete"}
re := regexp.MustCompile(`(/watch/)(.*?$)`)
matches := re.FindAllSubmatch([]byte(req.URL.Path), -1)
// len matches is always 3
// matches comes as e.g.
// [["...", "/watch/" "bucket1"]]
// [["/watch/" "/watch/" ""]]
// bucket name is on the second group, third position
wOptions.BucketName = strings.TrimSpace(string(matches[0][2]))
events := req.FormValue("events")
if strings.TrimSpace(events) != "" {
wOptions.Events = strings.Split(events, ",")
}
wOptions.Prefix = req.FormValue("prefix")
wOptions.Suffix = req.FormValue("suffix")
return wOptions
}

291
restapi/user_watch_test.go Normal file
View File

@@ -0,0 +1,291 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 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 restapi
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"testing"
"github.com/gorilla/websocket"
mc "github.com/minio/mc/cmd"
"github.com/minio/mc/pkg/probe"
"github.com/stretchr/testify/assert"
)
// assigning mock at runtime instead of compile time
var mcWatchMock func(options mc.WatchOptions) (*mc.WatchObject, *probe.Error)
// implements mc.S3Client.Watch()
func (c s3ClientMock) watch(options mc.WatchOptions) (*mc.WatchObject, *probe.Error) {
return mcWatchMock(options)
}
func TestWatch(t *testing.T) {
assert := assert.New(t)
client := s3ClientMock{}
mockWSConn := mockConn{}
function := "startWatch()"
testStreamSize := 5
testReceiver := make(chan []mc.EventInfo, testStreamSize)
textToReceive := "test message"
testOptions := watchOptions{}
testOptions.BucketName = "bucktest"
testOptions.Prefix = "file/"
testOptions.Suffix = ".png"
// Test-1: Serve Watch with no errors until Watch finishes sending
// define mock function behavior
mcWatchMock = func(params mc.WatchOptions) (*mc.WatchObject, *probe.Error) {
wo := &mc.WatchObject{
EventInfoChan: make(chan []mc.EventInfo),
ErrorChan: make(chan *probe.Error),
DoneChan: make(chan struct{}),
}
// Only success, start a routine to start reading line by line.
go func(wo *mc.WatchObject) {
defer wo.Close()
lines := make([]int, testStreamSize)
// mocking sending 5 lines of info
for range lines {
info := []mc.EventInfo{
mc.EventInfo{
UserAgent: textToReceive,
},
}
wo.Events() <- info
}
}(wo)
return wo, nil
}
// mock function of conn.ReadMessage(), no error on read
connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, nil
}
writesCount := 1
// mock connection WriteMessage() no error
connWriteMessageMock = func(messageType int, data []byte) error {
// emulate that receiver gets the message written
var t []mc.EventInfo
_ = json.Unmarshal(data, &t)
if writesCount == testStreamSize {
// for testing we need to close the receiver channel
close(testReceiver)
return nil
}
testReceiver <- t
writesCount++
return nil
}
if err := startWatch(mockWSConn, client, testOptions); err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
}
// check that the TestReceiver got the same number of data from Console.
for i := range testReceiver {
for _, val := range i {
assert.Equal(textToReceive, val.UserAgent)
}
}
// Test-2: if error happens while writing, return error
connWriteMessageMock = func(messageType int, data []byte) error {
return fmt.Errorf("error on write")
}
if err := startWatch(mockWSConn, client, testOptions); assert.Error(err) {
assert.Equal("error on write", err.Error())
}
// Test-3: error happens while reading, unexpected Close Error should return error.
connWriteMessageMock = func(messageType int, data []byte) error {
return nil
}
// mock function of conn.ReadMessage(), returns unexpected Close Error CloseAbnormalClosure
connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseAbnormalClosure, Text: ""}
}
if err := startWatch(mockWSConn, client, testOptions); assert.Error(err) {
assert.Equal("websocket: close 1006 (abnormal closure)", err.Error())
}
// Test-4: error happens while reading, expected Close Error NormalClosure
// expected Close Error should not return an error, just end Console.
connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseNormalClosure, Text: ""}
}
if err := startWatch(mockWSConn, client, testOptions); err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
}
// Test-5: error happens while reading, expected Close Error CloseGoingAway
// expected Close Error should not return an error, just return.
connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseGoingAway, Text: ""}
}
if err := startWatch(mockWSConn, client, testOptions); err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
}
// Test-6: error happens while reading, non Close Error Type should be returned as
// error
connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, fmt.Errorf("error on read")
}
if err := startWatch(mockWSConn, client, testOptions); assert.Error(err) {
assert.Equal("error on read", err.Error())
}
// Test-7: error happens on Watch, watch should stop
// and error shall be returned.
mcWatchMock = func(params mc.WatchOptions) (*mc.WatchObject, *probe.Error) {
wo := &mc.WatchObject{
EventInfoChan: make(chan []mc.EventInfo),
ErrorChan: make(chan *probe.Error),
DoneChan: make(chan struct{}),
}
// Only success, start a routine to start reading line by line.
go func(wo *mc.WatchObject) {
defer wo.Close()
lines := make([]int, testStreamSize)
// mocking sending 5 lines of info
for range lines {
info := []mc.EventInfo{
mc.EventInfo{
UserAgent: textToReceive,
},
}
wo.Events() <- info
}
wo.Errors() <- &probe.Error{Cause: fmt.Errorf("error on Watch")}
}(wo)
return wo, nil
}
// mock function of conn.ReadMessage(), no error on read, should stay unless
// context is done.
connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, nil
}
if err := startWatch(mockWSConn, client, testOptions); assert.Error(err) {
assert.Equal("error on Watch", err.Error())
}
// Test-8: error happens on Watch, watch should stop
// and error shall be returned.
mcWatchMock = func(params mc.WatchOptions) (*mc.WatchObject, *probe.Error) {
return nil, &probe.Error{Cause: fmt.Errorf("error on Watch")}
}
if err := startWatch(mockWSConn, client, testOptions); assert.Error(err) {
assert.Equal("error on Watch", err.Error())
}
// Test-9: return nil on error on Watch
mcWatchMock = func(params mc.WatchOptions) (*mc.WatchObject, *probe.Error) {
wo := &mc.WatchObject{
EventInfoChan: make(chan []mc.EventInfo),
ErrorChan: make(chan *probe.Error),
DoneChan: make(chan struct{}),
}
// Only success, start a routine to start reading line by line.
go func(wo *mc.WatchObject) {
defer wo.Close()
lines := make([]int, testStreamSize)
// mocking sending 5 lines of info
for range lines {
info := []mc.EventInfo{
mc.EventInfo{
UserAgent: textToReceive,
},
}
wo.Events() <- info
}
wo.Events() <- nil
wo.Errors() <- nil
}(wo)
return wo, nil
}
if err := startWatch(mockWSConn, client, testOptions); err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
}
// check that the TestReceiver got the same number of data from Console.
for i := range testReceiver {
for _, val := range i {
assert.Equal(textToReceive, val.UserAgent)
}
}
// Test-9: getOptionsFromReq return parameters from path
u, err := url.Parse("http://localhost/api/v1/watch/bucket1?prefix=&suffix=.jpg&events=put,get")
if err != nil {
t.Errorf("Failed on %s:, error occurred: %s", "url.Parse()", err.Error())
}
req := &http.Request{
URL: u,
}
opts := getOptionsFromReq(req)
expectedOptions := watchOptions{
BucketName: "bucket1",
}
expectedOptions.Prefix = ""
expectedOptions.Suffix = ".jpg"
expectedOptions.Events = []string{"put", "get"}
assert.Equal(expectedOptions.BucketName, opts.BucketName)
assert.Equal(expectedOptions.Prefix, opts.Prefix)
assert.Equal(expectedOptions.Suffix, opts.Suffix)
assert.Equal(expectedOptions.Events, opts.Events)
// Test-9: getOptionsFromReq return default events if not defined
u, err = url.Parse("http://localhost/api/v1/watch/bucket1?prefix=&suffix=.jpg&events=")
if err != nil {
t.Errorf("Failed on %s:, error occurred: %s", "url.Parse()", err.Error())
}
req = &http.Request{
URL: u,
}
opts = getOptionsFromReq(req)
expectedOptions = watchOptions{
BucketName: "bucket1",
}
expectedOptions.Prefix = ""
expectedOptions.Suffix = ".jpg"
expectedOptions.Events = []string{"put", "get", "delete"}
assert.Equal(expectedOptions.BucketName, opts.BucketName)
assert.Equal(expectedOptions.Prefix, opts.Prefix)
assert.Equal(expectedOptions.Suffix, opts.Suffix)
assert.Equal(expectedOptions.Events, opts.Events)
// Test-10: getOptionsFromReq return default events if not defined
u, err = url.Parse("http://localhost/api/v1/watch/bucket2?prefix=&suffix=")
if err != nil {
t.Errorf("Failed on %s:, error occurred: %s", "url.Parse()", err.Error())
}
req = &http.Request{
URL: u,
}
opts = getOptionsFromReq(req)
expectedOptions = watchOptions{
BucketName: "bucket2",
}
expectedOptions.Events = []string{"put", "get", "delete"}
assert.Equal(expectedOptions.BucketName, opts.BucketName)
assert.Equal(expectedOptions.Prefix, opts.Prefix)
assert.Equal(expectedOptions.Suffix, opts.Suffix)
assert.Equal(expectedOptions.Events, opts.Events)
}

View File

@@ -16,6 +16,8 @@
package restapi
import "os"
// DifferenceArrays returns the elements in `a` that aren't in `b`.
func DifferenceArrays(a, b []string) []string {
mb := make(map[string]struct{}, len(b))
@@ -54,3 +56,12 @@ func UniqueKeys(a []string) []string {
}
return list
}
// FileExists verifies if a file exist on the desired location and its not a folder
func FileExists(filename string) bool {
info, err := os.Stat(filename)
if os.IsNotExist(err) {
return false
}
return !info.IsDir()
}

View File

@@ -27,6 +27,7 @@ import (
"github.com/go-openapi/errors"
"github.com/gorilla/websocket"
"github.com/minio/mcs/pkg/auth"
"github.com/minio/mcs/pkg/ws"
)
@@ -46,17 +47,29 @@ const (
maxMessageSize = 512
)
// MCSWebsocket interface of a Websocket Client
type MCSWebsocket interface {
// start trace info from servers
// MCSWebsocketAdmin interface of a Websocket Client
type MCSWebsocketAdmin interface {
trace()
console()
}
type wsClient struct {
type wsAdminClient struct {
// websocket connection.
conn wsConn
// MinIO admin Client
madmin MinioAdmin
client MinioAdmin
}
// MCSWebsocket interface of a Websocket Client
type MCSWebsocket interface {
watch(options watchOptions)
}
type wsS3Client struct {
// websocket connection.
conn wsConn
// mcS3Client
client MCS3Client
}
// WSConn interface with all functions to be implemented
@@ -106,14 +119,18 @@ func (c wsConn) readMessage() (messageType int, p []byte, err error) {
// Websocket communication will be done depending
// on the path.
// Request should come like ws://<host>:<port>/ws/<api>
//
// TODO: Enable CORS
func serveWS(w http.ResponseWriter, req *http.Request) {
sessionID, err := ws.GetTokenFromRequest(req)
if err != nil {
errors.ServeError(w, req, err)
return
}
// Perform authentication before upgrading to a Websocket Connection
// authenticate WS connection with MCS
claims, err := ws.Authenticate(req)
claims, err := auth.JWTAuthenticate(*sessionID)
if err != nil {
log.Print("error on ws authentication: ", err)
errors.ServeError(w, req, err)
errors.ServeError(w, req, errors.New(http.StatusUnauthorized, err.Error()))
return
}
@@ -125,28 +142,30 @@ func serveWS(w http.ResponseWriter, req *http.Request) {
return
}
// Only start Websocket Interaction after user has been
// authenticated with MinIO
mAdmin, err := newAdminFromClaims(claims)
if err != nil {
log.Println("error creating Madmin Client:", err)
errors.ServeError(w, req, err)
return
}
// create a minioClient interface implementation
// defining the client to be used
adminClient := adminClient{client: mAdmin}
// create a websocket connection interface implementation
// defining the connection to be used
wsConnection := wsConn{conn: conn}
// create websocket client and handle request
wsClient := &wsClient{conn: wsConnection, madmin: adminClient}
switch strings.TrimPrefix(req.URL.Path, wsBasePath) {
case "/trace":
go wsClient.trace()
case "/console":
go wsClient.console()
wsPath := strings.TrimPrefix(req.URL.Path, wsBasePath)
switch {
case wsPath == "/trace":
wsAdminClient, err := newWebSocketAdminClient(conn, claims)
if err != nil {
errors.ServeError(w, req, err)
return
}
go wsAdminClient.trace()
case wsPath == "/console":
wsAdminClient, err := newWebSocketAdminClient(conn, claims)
if err != nil {
errors.ServeError(w, req, err)
return
}
go wsAdminClient.console()
case strings.HasPrefix(wsPath, `/watch`):
wOptions := getOptionsFromReq(req)
wsS3Client, err := newWebSocketS3Client(conn, *sessionID, wOptions.BucketName)
if err != nil {
errors.ServeError(w, req, err)
return
}
go wsS3Client.watch(wOptions)
default:
// path not found
conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
@@ -154,6 +173,51 @@ func serveWS(w http.ResponseWriter, req *http.Request) {
}
}
// newWebSocketAdminClient returns a wsAdminClient authenticated as an admin user
func newWebSocketAdminClient(conn *websocket.Conn, autClaims *auth.DecryptedClaims) (*wsAdminClient, error) {
// Only start Websocket Interaction after user has been
// authenticated with MinIO
mAdmin, err := newAdminFromClaims(autClaims)
if err != nil {
log.Println("error creating Madmin Client:", err)
// close connection
conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
conn.Close()
return nil, err
}
// create a websocket connection interface implementation
// defining the connection to be used
wsConnection := wsConn{conn: conn}
// create a minioClient interface implementation
// defining the client to be used
adminClient := adminClient{client: mAdmin}
// create websocket client and handle request
wsAdminClient := &wsAdminClient{conn: wsConnection, client: adminClient}
return wsAdminClient, nil
}
// newWebSocketS3Client returns a wsAdminClient authenticated as MCS admin
func newWebSocketS3Client(conn *websocket.Conn, jwt, bucketName string) (*wsS3Client, error) {
// Only start Websocket Interaction after user has been
// authenticated with MinIO
s3Client, err := newS3BucketClient(jwt, bucketName)
if err != nil {
log.Println("error creating S3Client:", err)
conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
conn.Close()
return nil, err
}
// create a websocket connection interface implementation
// defining the connection to be used
wsConnection := wsConn{conn: conn}
// create a s3Client interface implementation
// defining the client to be used
mcS3C := mcS3Client{client: s3Client}
// create websocket client and handle request
wsS3Client := &wsS3Client{conn: wsConnection, client: mcS3C}
return wsS3Client, nil
}
// wsReadCheck ensures that the client is sending a message
// every `pingWait` seconds. If deadline exceeded or an error
// happened this will return, meaning it won't be able to ensure
@@ -206,7 +270,7 @@ func wsReadCheck(ctx context.Context, wg *sync.WaitGroup, conn WSConn) chan erro
// trace serves madmin.ServiceTraceInfo
// on a Websocket connection.
func (wsc *wsClient) trace() {
func (wsc *wsAdminClient) trace() {
defer func() {
log.Println("trace stopped")
// close connection after return
@@ -214,7 +278,7 @@ func (wsc *wsClient) trace() {
}()
log.Println("trace started")
err := startTraceInfo(wsc.conn, wsc.madmin)
err := startTraceInfo(wsc.conn, wsc.client)
// Send Connection Close Message indicating the Status Code
// see https://tools.ietf.org/html/rfc6455#page-45
if err != nil {
@@ -237,7 +301,7 @@ func (wsc *wsClient) trace() {
// console serves madmin.GetLogs
// on a Websocket connection.
func (wsc *wsClient) console() {
func (wsc *wsAdminClient) console() {
defer func() {
log.Println("console logs stopped")
// close connection after return
@@ -245,7 +309,36 @@ func (wsc *wsClient) console() {
}()
log.Println("console logs started")
err := startConsoleLog(wsc.conn, wsc.madmin)
err := startConsoleLog(wsc.conn, wsc.client)
// Send Connection Close Message indicating the Status Code
// see https://tools.ietf.org/html/rfc6455#page-45
if err != nil {
// If connection exceeded read deadline send Close
// Message Policy Violation code since we don't want
// to let the receiver figure out the read deadline.
// This is a generic code designed if there is a
// need to hide specific details about the policy.
if nErr, ok := err.(net.Error); ok && nErr.Timeout() {
wsc.conn.writeMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.ClosePolicyViolation, ""))
return
}
// else, internal server error
wsc.conn.writeMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseInternalServerErr, err.Error()))
return
}
// normal closure
wsc.conn.writeMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
}
func (wsc *wsS3Client) watch(params watchOptions) {
defer func() {
log.Println("watch stopped")
// close connection after return
wsc.conn.close()
}()
log.Println("watch started")
err := startWatch(wsc.conn, wsc.client, params)
// Send Connection Close Message indicating the Status Code
// see https://tools.ietf.org/html/rfc6455#page-45
if err != nil {

View File

@@ -51,18 +51,3 @@ func (c mockConn) setReadDeadline(t time.Time) error {
}
func (c mockConn) setPongHandler(h func(appData string) error) {
}
// Common mocks for MCSWebsocket interface
// assigning mock at runtime instead of compile time
var wsTraceMock func()
// Define a mock struct of wsClient interface implementation
type wsClientMock struct {
// MinIO admin Client
madmin MinioAdmin
}
// mock function of wsc.trace()
func (wsc wsClientMock) trace() {
wsTraceMock()
}