Compare commits

..

28 Commits

Author SHA1 Message Date
Daniel Valdivia
c49a7bbe3c Add details on Policy for Service Account (#132) 2020-05-21 12:11:16 -07:00
Alex
3bb317535c Added path to bucket details in acls list (#131) 2020-05-20 21:59:03 -05:00
Alex
989e6f3471 Added Service Accounts page to settings (#128)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-05-19 13:41:46 -07:00
Lenin Alevski
35d575e7ac Allow sign-in users without policy (#129) 2020-05-19 15:26:53 -05:00
Harshavardhana
92a8aab07d upgrade linter and cleanup makefile (#126) 2020-05-18 21:55:54 -07:00
César Nieto
c5b2419191 Fix websocket apis (#127)
Remove ping check and instead use a context that will be canceled
if it the client sends a close message or an error occurs on reading.
The context will be used to cancel all functions using it.
2020-05-18 21:21:02 -07:00
Lenin Alevski
732e0ef683 ACL for mcs (#123)
This PR sets the initial version of the ACL for mcs, the idea behind
this is to start using the principle of least privileges when assigning
policies to users when creating users through mcs, currently mcsAdmin policy uses admin:*
and s3:* and by default a user with that policy will have access to everything, if want to limit
that we can create a policy with least privileges.

We need to start validating explicitly if users has acccess to an
specific endpoint based on IAM policy actions.

In this first version every endpoint (you can see it as a page to),
defines a set of well defined admin/s3 actions to work properly, ie:

```
// corresponds to /groups endpoint used by the groups page
var groupsActionSet = iampolicy.NewActionSet(
    iampolicy.ListGroupsAdminAction,
    iampolicy.AddUserToGroupAdminAction,
    //iampolicy.GetGroupAdminAction,
    iampolicy.EnableGroupAdminAction,
    iampolicy.DisableGroupAdminAction,
)

// corresponds to /policies endpoint used by the policies page
var iamPoliciesActionSet = iampolicy.NewActionSet(
    iampolicy.GetPolicyAdminAction,
    iampolicy.DeletePolicyAdminAction,
    iampolicy.CreatePolicyAdminAction,
    iampolicy.AttachPolicyAdminAction,
    iampolicy.ListUserPoliciesAdminAction,
)
```
With that said, for this initial version, now the sessions endpoint will
return a list of authorized pages to be render on the UI, on subsequent
prs we will add this verification of authorization via a server
middleware.
2020-05-18 18:03:06 -07:00
César Nieto
e8491d80cb Add size info to bucket list api (#122)
Using madmin.AccountUsageInfo since that api
includes size already.
Also includes integration with UI.
2020-05-18 13:36:18 -07:00
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
Daniel Valdivia
cf8472b04c wss for websockets on tls and single page application behavior (#107) 2020-05-08 16:36:08 -07:00
Alex
317a7ebbd3 Fixed error messages in mcs (#105)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-05-08 12:31:09 -07:00
Alex
63f4150232 Added pagination to users page (#100)
Added pagination to users page for mcs, this resolves #96

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-05-06 21:47:41 -07:00
César Nieto
3bfc2556fc authenticate websocket apis using sts (#97) 2020-05-06 16:47:49 -05:00
César Nieto
511cc47d2b Add console logs api and integrate it with UI (#90)
Uses same behavior as the Trace feature using websockets.
For displaying it on the UI it needed to handle colors
since the log message comes with unicode colors embbeded
on the message.
Also a special case when an error log comes needed to be handled
to show all sources of the error.
2020-05-05 15:12:04 -07:00
Alex
9660650f41 Settings forms connection (#95)
Connected the forms to backend to send & receive the information stored in MinIO settings

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-05-05 11:07:06 -07:00
Lenin Alevski
9ac754d4de MCS use the correct region to authenticate users (#94)
Previous mcs was authenticating all the users agains <empty> region,
this was a problem when an admin configure a different region via the
configuration page on mcs, now before authenticating a user via
credentials or idp mcs will ask minio what's the current region and try
to authenticate using that that information.

- Login to mcs
- Go to the configuration page and change the region, ie: us-west-1
- Logout from mcs
- Login to mcs again, you should not get any error
2020-05-04 18:18:04 -05:00
César Nieto
646318e1f6 Add list and delete service accounts api (#91) 2020-05-04 15:48:38 -07:00
César Nieto
beb1ac7d04 add CREDITS file (#93) 2020-05-04 15:41:16 -07:00
César Nieto
f3a9311374 add comments to exported functions (#92) 2020-05-04 15:28:49 -07:00
121 changed files with 30565 additions and 2969 deletions

View File

@@ -18,21 +18,19 @@ jobs:
os: [ubuntu-latest]
steps:
- name: Set up Go ${{ matrix.go-version }} on ${{ matrix.os }}
uses: actions/setup-go@v1
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v1
uses: actions/checkout@v2
- name: Build on ${{ matrix.os }}
env:
GO111MODULE: on
GOOS: linux
run: |
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.24.0
$(go env GOPATH)/bin/golangci-lint run --timeout=5m --config ./.golangci.yml
go mod vendor
go test -v -race ./...
make verifiers
make test
make mcs

View File

@@ -1,4 +1,7 @@
linters-settings:
golint:
min-confidence: 0
misspell:
locale: US
@@ -14,4 +17,8 @@ linters:
- gosimple
- deadcode
- unparam
- unused
- structcheck
service:
golangci-lint-version: 1.21.0 # use the fixed version to not introduce new linters unexpectedly

23788
CREDITS Normal file

File diff suppressed because it is too large Load Diff

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

@@ -6,7 +6,23 @@ default: mcs
.PHONY: mcs
mcs:
@echo "Building mcs binary to './mcs'"
@(CGO_ENABLED=0 go build -trimpath --tags=kqueue --ldflags "-s -w" -o mcs ./cmd/mcs)
@(GO111MODULE=on CGO_ENABLED=0 go build -trimpath --tags=kqueue --ldflags "-s -w" -o mcs ./cmd/mcs)
getdeps:
@mkdir -p ${GOPATH}/bin
@which golangci-lint 1>/dev/null || (echo "Installing golangci-lint" && curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH)/bin v1.27.0)
verifiers: getdeps fmt lint
fmt:
@echo "Running $@ check"
@GO111MODULE=on gofmt -d cmd/
@GO111MODULE=on gofmt -d pkg/
lint:
@echo "Running $@ check"
@GO111MODULE=on ${GOPATH}/bin/golangci-lint cache clean
@GO111MODULE=on ${GOPATH}/bin/golangci-lint run --timeout=5m --config ./.golangci.yml
install: mcs
@echo "Installing mcs binary to '$(GOPATH)/bin/mcs'"
@@ -21,11 +37,11 @@ assets:
@(cd portal-ui; yarn install; make build-static; cd ..)
test:
@(go test -race -v github.com/minio/mcs/restapi/...)
@(go test -race -v github.com/minio/mcs/pkg/auth/...)
@(GO111MODULE=on go test -race -v github.com/minio/mcs/restapi/...)
@(GO111MODULE=on go test -race -v github.com/minio/mcs/pkg/...)
coverage:
@(go test -v -coverprofile=coverage.out github.com/minio/mcs/restapi/... && go tool cover -html=coverage.out && open coverage.html)
@(GO111MODULE=on go test -v -coverprofile=coverage.out github.com/minio/mcs/restapi/... && go tool cover -html=coverage.out && open coverage.html)
clean:
@echo "Cleaning up all the generated files"

View File

@@ -14,31 +14,30 @@ $ mc admin user add myminio mcs YOURMCSSECRET
$ set -o history
```
2. Create a policy for `mcs`
2. Create a policy for `mcs` with access to everything (for testing and debugging)
```
$ cat > mcsAdmin.json << EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"admin:*"
],
"Effect": "Allow",
"Sid": ""
},
{
"Action": [
"s3:*"
],
"Effect": "Allow",
"Resource": [
"arn:aws:s3:::*"
],
"Sid": ""
}
]
"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
@@ -50,6 +49,49 @@ $ mc admin policy add myminio mcsAdmin mcsAdmin.json
$ mc admin policy set myminio mcsAdmin user=mcs
```
### Note
Additionally, you can create policies to limit the privileges for `mcs` users, for example, if you want the user to only have access to dashboard, buckets, notifications and watch page, the policy should look like this:
```
{
"Version": "2012-10-17",
"Statement": [{
"Action": [
"admin:ServerInfo",
],
"Effect": "Allow",
"Sid": ""
},
{
"Action": [
"s3:ListenBucketNotification",
"s3:PutBucketNotification",
"s3:GetBucketNotification",
"s3:ListMultipartUploadParts",
"s3:ListBucketMultipartUploads",
"s3:ListBucket",
"s3:HeadBucket",
"s3:GetObject",
"s3:GetBucketLocation",
"s3:AbortMultipartUpload",
"s3:CreateBucket",
"s3:PutObject",
"s3:DeleteObject",
"s3:DeleteBucket",
"s3:PutBucketPolicy",
"s3:DeleteBucketPolicy",
"s3:GetBucketPolicy"
],
"Effect": "Allow",
"Resource": [
"arn:aws:s3:::*"
],
"Sid": ""
}
]
}
```
## Run MCS server
To run the server:
@@ -68,6 +110,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

6
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/minio v0.0.0-20200428222040-c3c3e9087bc1
github.com/minio/minio-go/v6 v6.0.55-0.20200424204115-7506d2996b22
github.com/minio/mc v0.0.0-20200515235434-3b479cf92ed6
github.com/minio/minio v0.0.0-20200516011754-9cac385aecdb
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

57
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,8 +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 h1:nWDRPCyCltiTsANwC/n3QZH7Vww33Npq9MKqlwRzI/c=
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=
@@ -42,8 +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 h1:ZNv7On9kyUzm7fvRZumSyy/IUiSC7AzL0I1jKKtwooA=
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=
@@ -116,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=
@@ -241,6 +239,7 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -340,12 +339,16 @@ github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eT
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/cpuid v1.2.2 h1:1xAgYebNnsb9LKCdLOvFWtAxGU/33mjJtyOVbmUa0Us=
github.com/klauspost/cpuid v1.2.2/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid v1.2.4 h1:EBfaK0SWSwk+fgk6efYFWdzl8MwRWoOO1gkmiaTXPW4=
github.com/klauspost/cpuid v1.2.4/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.1 h1:oIPZROsWuPHpOdMVWLuJZXwgjhrW8r1yEX8UqMyeNHM=
github.com/klauspost/pgzip v1.2.1/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/klauspost/readahead v1.3.1 h1:QqXNYvm+VvqYcbrRT4LojUciM0XrznFRIDrbHiJtu/0=
github.com/klauspost/readahead v1.3.1/go.mod h1:AH9juHzNH7xqdqFHrMRSHeH2Ps+vFf+kblDqzPFiLJg=
github.com/klauspost/reedsolomon v1.9.3 h1:N/VzgeMfHmLc+KHMD1UL/tNkfXAt8FnUqlgXGIduwAY=
github.com/klauspost/reedsolomon v1.9.3/go.mod h1:CwCi+NUr9pqSVktrkN+Ondf06rkhYZ/pcNv7fu+8Un4=
github.com/klauspost/reedsolomon v1.9.7 h1:+azeqnT4iNG9UEcWC+7utJ4xXQ9S8pSlkZor0DOArEQ=
github.com/klauspost/reedsolomon v1.9.7/go.mod h1:+8WD025Xpby8/kG5h/HDPIFhiiuGEtZOKw+5Y4drAD8=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -401,16 +404,16 @@ 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 h1:9hlqghJl3e3HorXa6ADWsz6ECq790t4iQs07VD9JctM=
github.com/minio/minio v0.0.0-20200415191640-bde0f444dbab/go.mod h1:v8oQPMMaTkjDwp5cOz1WCElA4Ik+X+0y4On+VMk0fis=
github.com/minio/minio v0.0.0-20200428222040-c3c3e9087bc1 h1:DQjH/653WCerOeZCp3BxAgkmRiQybHYiprbTFs+brgA=
github.com/minio/minio v0.0.0-20200428222040-c3c3e9087bc1/go.mod h1:HxnN5FYGIii8ZH6d+LH5UNOSSIonbJkYPqP6gWelVO0=
github.com/minio/mc v0.0.0-20200515235434-3b479cf92ed6 h1:2SrKe2vLDLwvnYkYrJelrzyGW8t/8HCbr9yDsw+8XSI=
github.com/minio/mc v0.0.0-20200515235434-3b479cf92ed6/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-20200516011754-9cac385aecdb h1:CQC7D3UDnUycuxhwImcVhMSLet/RbShosAnYcvMtEB8=
github.com/minio/minio v0.0.0-20200516011754-9cac385aecdb/go.mod h1:wymaytM/HELuwdz7BGZHmQ3XKq2SxPsLeGxyOCaCLiA=
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.55-0.20200425081427-89eebdef2af0/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=
@@ -555,6 +558,7 @@ github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94 h1:0ngsPmuP6XIjiFRN
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@@ -596,6 +600,7 @@ github.com/xdg/stringprep v1.0.0 h1:d9X0esnoa3dFsV0FG35rAT0RIhYFlPq7MiP+DW89La0=
github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
@@ -608,11 +613,19 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.11.0 h1:gSmpCfs+R47a4yQPAI4xJ0IPDLTRGXskm6UelqNXpqE=
go.uber.org/zap v1.11.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.15.0 h1:ZZCA22JRF2gQE5FoNmhmrf7jeJJ2uhqDUNRYKm8dvmM=
go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=
golang.org/x/arch v0.0.0-20190909030613-46d78d1859ac/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -621,11 +634,13 @@ golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191117063200-497ca9f6d64f h1:kz4KIr+xcPUsI3VMoqWfPMvtnJ6MGfiVwsWSVzphMO4=
golang.org/x/crypto v0.0.0-20191117063200-497ca9f6d64f/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6 h1:Sy5bstxEqwwbYs6n0/pBuxKENqOeZUgD45Gp3Q3pqLg=
@@ -635,6 +650,11 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -665,6 +685,8 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -711,9 +733,18 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn
golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190914235951-31e00f45c22e h1:nOOVVcLC+/3MeovP40q5lCiWmP1Z1DaN8yn8ngU63hw=
golang.org/x/tools v0.0.0-20190914235951-31e00f45c22e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200428211428-0c9eba77bc32 h1:Xvf3ZQTm5bjXPxhI7g+dwqsCqadK1rcNtwtszuatetk=
golang.org/x/tools v0.0.0-20200428211428-0c9eba77bc32/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.5.0 h1:lj9SyhMzyoa38fgFF0oO2T6pjs5IzkLPKfVtxpyCRMM=
google.golang.org/api v0.5.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
@@ -776,4 +807,6 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@@ -27,22 +27,22 @@ import (
"github.com/go-openapi/swag"
)
// ServiceAccount service account
// ServiceAccountRequest service account request
//
// swagger:model serviceAccount
type ServiceAccount struct {
// swagger:model serviceAccountRequest
type ServiceAccountRequest struct {
// policy to be applied to the Service Account if any
Policy string `json:"policy,omitempty"`
}
// Validate validates this service account
func (m *ServiceAccount) Validate(formats strfmt.Registry) error {
// Validate validates this service account request
func (m *ServiceAccountRequest) Validate(formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *ServiceAccount) MarshalBinary() ([]byte, error) {
func (m *ServiceAccountRequest) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
@@ -50,8 +50,8 @@ func (m *ServiceAccount) MarshalBinary() ([]byte, error) {
}
// UnmarshalBinary interface implementation
func (m *ServiceAccount) UnmarshalBinary(b []byte) error {
var res ServiceAccount
func (m *ServiceAccountRequest) UnmarshalBinary(b []byte) error {
var res ServiceAccountRequest
if err := swag.ReadJSON(b, &res); err != nil {
return err
}

View File

@@ -0,0 +1,37 @@
// Code generated by go-swagger; DO NOT EDIT.
// 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 models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"github.com/go-openapi/strfmt"
)
// ServiceAccounts service accounts
//
// swagger:model serviceAccounts
type ServiceAccounts []string
// Validate validates this service accounts
func (m ServiceAccounts) Validate(formats strfmt.Registry) error {
return nil
}

View File

@@ -36,6 +36,9 @@ import (
// swagger:model sessionResponse
type SessionResponse struct {
// pages
Pages []string `json:"pages"`
// status
// Enum: [ok]
Status string `json:"status,omitempty"`

278
pkg/acl/endpoints.go Normal file
View File

@@ -0,0 +1,278 @@
// 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 acl
import iampolicy "github.com/minio/minio/pkg/iam/policy"
// endpoints definition
var (
configuration = "/configurations-list"
users = "/users"
groups = "/groups"
iamPolicies = "/policies"
dashboard = "/dashboard"
profiling = "/profiling"
trace = "/trace"
logs = "/logs"
watch = "/watch"
notifications = "/notification-endpoints"
buckets = "/buckets"
bucketDetails = "/buckets/:bucketName"
serviceAccounts = "/service-accounts"
)
type ConfigurationActionSet struct {
actionTypes iampolicy.ActionSet
actions iampolicy.ActionSet
}
// configurationActionSet contains the list of admin actions required for this endpoint to work
var configurationActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.ConfigUpdateAdminAction,
),
}
// logsActionSet contains the list of admin actions required for this endpoint to work
var logsActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.ConsoleLogAdminAction,
),
}
// dashboardActionSet contains the list of admin actions required for this endpoint to work
var dashboardActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.ServerInfoAdminAction,
),
}
// groupsActionSet contains the list of admin actions required for this endpoint to work
var groupsActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.ListGroupsAdminAction,
iampolicy.AddUserToGroupAdminAction,
//iampolicy.GetGroupAdminAction,
iampolicy.EnableGroupAdminAction,
iampolicy.DisableGroupAdminAction,
),
}
// iamPoliciesActionSet contains the list of admin actions required for this endpoint to work
var iamPoliciesActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.GetPolicyAdminAction,
iampolicy.DeletePolicyAdminAction,
iampolicy.CreatePolicyAdminAction,
iampolicy.AttachPolicyAdminAction,
iampolicy.ListUserPoliciesAdminAction,
),
}
// profilingActionSet contains the list of admin actions required for this endpoint to work
var profilingActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.ProfilingAdminAction,
),
}
// traceActionSet contains the list of admin actions required for this endpoint to work
var traceActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.TraceAdminAction,
),
}
// usersActionSet contains the list of admin actions required for this endpoint to work
var usersActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.ListUsersAdminAction,
iampolicy.CreateUserAdminAction,
iampolicy.DeleteUserAdminAction,
iampolicy.GetUserAdminAction,
iampolicy.EnableUserAdminAction,
iampolicy.DisableUserAdminAction,
),
}
// watchActionSet contains the list of admin actions required for this endpoint to work
var watchActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.ListenBucketNotificationAction,
),
}
// notificationsActionSet contains the list of admin actions required for this endpoint to work
var notificationsActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllActions,
),
actions: iampolicy.NewActionSet(
iampolicy.ListenBucketNotificationAction,
iampolicy.PutBucketNotificationAction,
iampolicy.GetBucketNotificationAction,
),
}
// bucketsActionSet contains the list of admin actions required for this endpoint to work
var bucketsActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllActions,
),
actions: iampolicy.NewActionSet(
// Read access to buckets
iampolicy.ListMultipartUploadPartsAction,
iampolicy.ListBucketMultipartUploadsAction,
iampolicy.ListBucketAction,
iampolicy.HeadBucketAction,
iampolicy.GetObjectAction,
iampolicy.GetBucketLocationAction,
// Write access to buckets
iampolicy.AbortMultipartUploadAction,
iampolicy.CreateBucketAction,
iampolicy.PutObjectAction,
iampolicy.DeleteObjectAction,
iampolicy.DeleteBucketAction,
// Assign bucket policies
iampolicy.PutBucketPolicyAction,
iampolicy.DeleteBucketPolicyAction,
iampolicy.GetBucketPolicyAction,
),
}
// serviceAccountsActionSet contains the list of admin actions required for this endpoint to work
var serviceAccountsActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(),
actions: iampolicy.NewActionSet(),
}
// endpointRules contains the mapping between endpoints and ActionSets, additional rules can be added here
var endpointRules = map[string]ConfigurationActionSet{
configuration: configurationActionSet,
users: usersActionSet,
groups: groupsActionSet,
iamPolicies: iamPoliciesActionSet,
dashboard: dashboardActionSet,
profiling: profilingActionSet,
trace: traceActionSet,
logs: logsActionSet,
watch: watchActionSet,
notifications: notificationsActionSet,
buckets: bucketsActionSet,
bucketDetails: bucketsActionSet,
serviceAccounts: serviceAccountsActionSet,
}
// GetActionsStringFromPolicy extract the admin/s3 actions from a given policy and return them in []string format
//
// ie:
// {
// "Version": "2012-10-17",
// "Statement": [{
// "Action": [
// "admin:ServerInfo",
// "admin:CreatePolicy",
// "admin:GetUser"
// ],
// ...
// },
// {
// "Action": [
// "s3:ListenBucketNotification",
// "s3:PutBucketNotification"
// ],
// ...
// }
// ]
// }
// Will produce an array like: ["admin:ServerInfo", "admin:CreatePolicy", "admin:GetUser", "s3:ListenBucketNotification", "s3:PutBucketNotification"]\
func GetActionsStringFromPolicy(policy *iampolicy.Policy) []string {
var actions []string
for _, statement := range policy.Statements {
// We only care about allowed actions
if statement.Effect.IsAllowed(true) {
for _, action := range statement.Actions.ToSlice() {
actions = append(actions, string(action))
}
}
}
return actions
}
// actionsStringToActionSet convert a given string array to iampolicy.ActionSet structure
// this avoids ending with duplicate actions
func actionsStringToActionSet(actions []string) iampolicy.ActionSet {
actionsSet := iampolicy.ActionSet{}
for _, action := range actions {
actionsSet.Add(iampolicy.Action(action))
}
return actionsSet
}
// GetAuthorizedEndpoints return a list of allowed endpoint based on a provided *iampolicy.Policy
// ie: pages the user should have access based on his current privileges
func GetAuthorizedEndpoints(actions []string) []string {
if len(actions) == 0 {
return []string{}
}
// Prepare new ActionSet structure that will hold all the user actions
userAllowedAction := actionsStringToActionSet(actions)
allowedEndpoints := []string{}
for endpoint, rules := range endpointRules {
// check if user policy matches s3:* or admin:* typesIntersection
endpointActionTypes := rules.actionTypes
typesIntersection := endpointActionTypes.Intersection(userAllowedAction)
if len(typesIntersection) == len(endpointActionTypes.ToSlice()) {
allowedEndpoints = append(allowedEndpoints, endpoint)
continue
}
// check if user policy matches explicitly defined endpoint required actions
endpointRequiredActions := rules.actions
actionsIntersection := endpointRequiredActions.Intersection(userAllowedAction)
if len(actionsIntersection) == len(endpointRequiredActions.ToSlice()) {
allowedEndpoints = append(allowedEndpoints, endpoint)
}
}
return allowedEndpoints
}

138
pkg/acl/endpoints_test.go Normal file
View File

@@ -0,0 +1,138 @@
// 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 acl
import (
"reflect"
"testing"
iampolicy "github.com/minio/minio/pkg/iam/policy"
)
func TestGetAuthorizedEndpoints(t *testing.T) {
type args struct {
actions []string
}
tests := []struct {
name string
args args
want int
}{
{
name: "dashboard endpoint",
args: args{
[]string{"admin:ServerInfo"},
},
want: 2,
},
{
name: "policies endpoint",
args: args{
[]string{
"admin:CreatePolicy",
"admin:DeletePolicy",
"admin:GetPolicy",
"admin:AttachUserOrGroupPolicy",
"admin:ListUserPolicies",
},
},
want: 2,
},
{
name: "all admin endpoints",
args: args{
[]string{
"admin:*",
},
},
want: 10,
},
{
name: "all s3 endpoints",
args: args{
[]string{
"s3:*",
},
},
want: 4,
},
{
name: "all admin and s3 endpoints",
args: args{
[]string{
"admin:*",
"s3:*",
},
},
want: 13,
},
{
name: "no endpoints",
args: args{
[]string{},
},
want: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := GetAuthorizedEndpoints(tt.args.actions); !reflect.DeepEqual(len(got), tt.want) {
t.Errorf("GetAuthorizedEndpoints() = %v, want %v", len(got), tt.want)
}
})
}
}
func TestGetActionsStringFromPolicy(t *testing.T) {
type args struct {
policy *iampolicy.Policy
}
tests := []struct {
name string
args args
want int
}{
{
name: "parse ReadOnly policy",
args: args{
policy: &iampolicy.ReadOnly,
},
want: 2,
},
{
name: "parse WriteOnly policy",
args: args{
policy: &iampolicy.WriteOnly,
},
want: 1,
},
{
name: "parse AdminDiagnostics policy",
args: args{
policy: &iampolicy.AdminDiagnostics,
},
want: 6,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := GetActionsStringFromPolicy(tt.args.policy); !reflect.DeepEqual(len(got), tt.want) {
t.Errorf("GetActionsStringFromPolicy() = %v, want %v", len(got), tt.want)
}
})
}
}

View File

@@ -37,7 +37,7 @@ import (
)
var (
errAuthentication = errors.New("Authentication failed, check your access credentials")
errAuthentication = errors.New("authentication failed, check your access credentials")
errNoAuthToken = errors.New("JWT token missing")
errReadingToken = errors.New("JWT internal data is malformed")
errClaimsFormat = errors.New("encrypted jwt claims not in the right format")
@@ -57,6 +57,7 @@ type DecryptedClaims struct {
AccessKeyID string
SecretAccessKey string
SessionToken string
Actions []string
}
// JWTAuthenticate takes a jwt, decode it, extract claims and validate the signature
@@ -93,9 +94,9 @@ func JWTAuthenticate(token string) (*DecryptedClaims, error) {
// NewJWTWithClaimsForClient generates a new jwt with claims based on the provided STS credentials, first
// encrypts the claims and the sign them
func NewJWTWithClaimsForClient(credentials *credentials.Value, audience string) (string, error) {
func NewJWTWithClaimsForClient(credentials *credentials.Value, actions []string, audience string) (string, error) {
if credentials != nil {
encryptedClaims, err := encryptClaims(credentials.AccessKeyID, credentials.SecretAccessKey, credentials.SessionToken)
encryptedClaims, err := encryptClaims(credentials.AccessKeyID, credentials.SecretAccessKey, credentials.SessionToken, actions)
if err != nil {
return "", err
}
@@ -112,8 +113,8 @@ func NewJWTWithClaimsForClient(credentials *credentials.Value, audience string)
// encryptClaims() receives the 3 STS claims, concatenate them and encrypt them using AES-GCM
// returns a base64 encoded ciphertext
func encryptClaims(accessKeyID, secretAccessKey, sessionToken string) (string, error) {
payload := []byte(fmt.Sprintf("%s:%s:%s", accessKeyID, secretAccessKey, sessionToken))
func encryptClaims(accessKeyID, secretAccessKey, sessionToken string, actions []string) (string, error) {
payload := []byte(fmt.Sprintf("%s#%s#%s#%s", accessKeyID, secretAccessKey, sessionToken, strings.Join(actions, ",")))
ciphertext, err := encrypt(payload)
if err != nil {
return "", err
@@ -133,16 +134,18 @@ func decryptClaims(ciphertext string) (*DecryptedClaims, error) {
log.Println(err)
return nil, errClaimsFormat
}
s := strings.Split(string(plaintext), ":")
s := strings.Split(string(plaintext), "#")
// Validate that the decrypted string has the right format "accessKeyID:secretAccessKey:sessionToken"
if len(s) != 3 {
if len(s) != 4 {
return nil, errClaimsFormat
}
accessKeyID, secretAccessKey, sessionToken := s[0], s[1], s[2]
accessKeyID, secretAccessKey, sessionToken, actions := s[0], s[1], s[2], s[3]
actionsList := strings.Split(actions, ",")
return &DecryptedClaims{
AccessKeyID: accessKeyID,
SecretAccessKey: secretAccessKey,
SessionToken: sessionToken,
Actions: actionsList,
}, nil
}

View File

@@ -37,14 +37,14 @@ func TestNewJWTWithClaimsForClient(t *testing.T) {
funcAssert := assert.New(t)
// Test-1 : NewJWTWithClaimsForClient() is generated correctly without errors
function := "NewJWTWithClaimsForClient()"
jwt, err := NewJWTWithClaimsForClient(creds, audience)
jwt, err := NewJWTWithClaimsForClient(creds, []string{""}, audience)
if err != nil || jwt == "" {
t.Errorf("Failed on %s:, error occurred: %s", function, err)
}
// saving jwt for future tests
goodToken = jwt
// Test-2 : NewJWTWithClaimsForClient() throws error because of empty credentials
if _, err = NewJWTWithClaimsForClient(nil, audience); err != nil {
if _, err = NewJWTWithClaimsForClient(nil, []string{""}, audience); err != nil {
funcAssert.Equal("provided credentials are empty", err.Error())
}
}
@@ -63,7 +63,7 @@ func TestJWTAuthenticate(t *testing.T) {
}
// Test-2 : JWTAuthenticate() return an error because of a tampered jwt
if _, err := JWTAuthenticate(badToken); err != nil {
funcAssert.Equal("Authentication failed, check your access credentials", err.Error())
funcAssert.Equal("authentication failed, check your access credentials", err.Error())
}
// Test-3 : JWTAuthenticate() return an error because of an empty jwt
if _, err := JWTAuthenticate(""); err != nil {

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

@@ -20,6 +20,7 @@
"@types/superagent": "^4.1.4",
"@types/webpack-env": "^1.14.1",
"@types/websocket": "^1.0.0",
"ansi-to-react": "^6.0.5",
"codemirror": "^5.52.2",
"history": "^4.10.1",
"local-storage-fallback": "^4.1.1",

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

@@ -35,7 +35,7 @@ const isLoggedIn = () => {
};
const mapState = (state: AppState) => ({
loggedIn: state.system.loggedIn
loggedIn: state.system.loggedIn,
});
const connector = connect(mapState, { userLoggedIn });

View File

@@ -16,6 +16,7 @@
import storage from "local-storage-fallback";
import request from "superagent";
import get from "lodash/get";
export class API {
invoke(method: string, url: string, data?: object) {
@@ -23,8 +24,8 @@ export class API {
return request(method, url)
.set("Authorization", `Bearer ${token}`)
.send(data)
.then(res => res.body)
.catch(err => {
.then((res) => res.body)
.catch((err) => {
// if we get unauthorized, kick out the user
if (err.status === 401) {
storage.removeItem("token");
@@ -36,10 +37,16 @@ export class API {
onError(err: any) {
if (err.status) {
const errMessage: string =
(err.response.body && err.response.body.error) ||
err.status.toString(10);
return Promise.reject(errMessage);
const errMessage = get(
err.response,
"body.message",
err.status.toString()
);
const throwMessage =
errMessage.charAt(0).toUpperCase() + errMessage.slice(1);
return Promise.reject(throwMessage);
} else {
return Promise.reject("Unknown error");
}

View File

@@ -14,7 +14,17 @@
// 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 const units = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
export const units = [
"B",
"KiB",
"MiB",
"GiB",
"TiB",
"PiB",
"EiB",
"ZiB",
"YiB",
];
export const niceBytes = (x: string) => {
let l = 0,
n = parseInt(x, 10) || 0;
@@ -38,3 +48,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

@@ -30,49 +30,50 @@ import AddBucket from "./AddBucket";
import DeleteBucket from "./DeleteBucket";
import { MinTablePaginationActions } from "../../../../common/MinTablePaginationActions";
import { CreateIcon } from "../../../../icons";
import { niceBytes } from "../../../../common/utils";
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",
},
});
interface IListBucketsProps {
@@ -108,7 +109,7 @@ class ListBuckets extends React.Component<
rowsPerPage: 10,
deleteOpen: false,
selectedBucket: "",
filterBuckets: ""
filterBuckets: "",
};
fetchRecords() {
@@ -122,7 +123,7 @@ class ListBuckets extends React.Component<
loading: false,
records: res.buckets || [],
totalRecords: !res.buckets ? 0 : res.total,
error: ""
error: "",
});
// if we get 0 results, and page > 0 , go down 1 page
if (
@@ -174,7 +175,7 @@ class ListBuckets extends React.Component<
rowsPerPage,
deleteOpen,
selectedBucket,
filterBuckets
filterBuckets,
} = this.state;
const offset = page * rowsPerPage;
@@ -196,7 +197,7 @@ class ListBuckets extends React.Component<
const tableActions = [
{ type: "view", to: `/buckets`, sendOnlyId: true },
{ type: "delete", onClick: confirmDeleteBucket, sendOnlyId: true }
{ type: "delete", onClick: confirmDeleteBucket, sendOnlyId: true },
];
const displayParsedDate = (date: string) => {
@@ -249,9 +250,9 @@ class ListBuckets extends React.Component<
className={classes.searchField}
id="search-resource"
label=""
onChange={val => {
onChange={(val) => {
this.setState({
filterBuckets: val.target.value
filterBuckets: val.target.value,
});
}}
InputProps={{
@@ -260,7 +261,7 @@ class ListBuckets extends React.Component<
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
),
}}
/>
<Button
@@ -269,7 +270,7 @@ class ListBuckets extends React.Component<
startIcon={<CreateIcon />}
onClick={() => {
this.setState({
addScreenOpen: true
addScreenOpen: true,
});
}}
>
@@ -287,8 +288,13 @@ class ListBuckets extends React.Component<
{
label: "Creation Date",
elementKey: "creation_date",
renderFunction: displayParsedDate
}
renderFunction: displayParsedDate,
},
{
label: "Size",
elementKey: "size",
renderFunction: niceBytes,
},
]}
isLoading={loading}
records={filteredRecords}
@@ -302,11 +308,11 @@ class ListBuckets extends React.Component<
page: page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true
native: true,
},
onChangePage: handleChangePage,
onChangeRowsPerPage: handleChangeRowsPerPage,
ActionsComponent: MinTablePaginationActions
ActionsComponent: MinTablePaginationActions,
}}
/>
</Grid>

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

@@ -13,19 +13,13 @@
//
// 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, {
useState,
useEffect,
createRef,
ChangeEvent,
useCallback
} from "react";
import React, { useState, useEffect, createRef, ChangeEvent } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
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 {
@@ -40,9 +34,10 @@ interface ICSVMultiSelector {
const styles = (theme: Theme) =>
createStyles({
...fieldBasic,
...tooltipHelper,
inputLabel: {
...fieldBasic.inputLabel,
marginBottom: 10
width: 116,
},
inputContainer: {
height: 150,
@@ -50,11 +45,10 @@ const styles = (theme: Theme) =>
padding: 15,
position: "relative",
border: "1px solid #c4c4c4",
marginBottom: 10
},
labelContainer: {
display: "flex"
}
display: "flex",
},
});
const CSVMultiSelector = ({
@@ -63,26 +57,30 @@ const CSVMultiSelector = ({
label,
tooltip = "",
onChange,
classes
classes,
}: ICSVMultiSelector) => {
const [currentElements, setCurrentElements] = useState<string[]>([""]);
const bottomList = createRef<HTMLDivElement>();
// Use effect to get the initial values from props
useCallback(() => {
if (currentElements.length === 1 && currentElements[0] === "") {
const elementsSplitted = elements.split(",");
if (elementsSplitted[elementsSplitted.length - 1].trim() !== "") {
elementsSplitted.push("");
}
setCurrentElements(elementsSplitted);
useEffect(() => {
if (
currentElements.length === 1 &&
currentElements[0] === "" &&
elements &&
elements !== ""
) {
const elementsSplit = elements.split(",");
elementsSplit.push("");
setCurrentElements(elementsSplit);
}
}, [elements, setCurrentElements, currentElements]);
}, [elements, currentElements]);
// 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
@@ -131,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

@@ -26,7 +26,7 @@ import {
TableRow,
Paper,
Grid,
Checkbox
Checkbox,
} from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { TablePaginationActionsProps } from "@material-ui/core/TablePagination/TablePaginationActions";
@@ -84,25 +84,25 @@ const borderColor = "#eaeaea";
const rowText = {
fontWeight: 400,
fontSize: 14,
borderColor: borderColor
borderColor: borderColor,
};
const checkBoxBasic = {
width: 16,
height: 16,
borderRadius: 3
borderRadius: 3,
};
const styles = (theme: Theme) =>
createStyles({
dialogContainer: {
padding: "12px 26px 22px"
padding: "12px 26px 22px",
},
paper: {
display: "flex",
overflow: "auto",
flexDirection: "column",
padding: "19px 38px"
padding: "19px 38px",
},
minTableHeader: {
color: "#393939",
@@ -111,43 +111,43 @@ const styles = (theme: Theme) =>
fontWeight: 700,
fontSize: 14,
paddingBottom: 15,
borderColor: borderColor
}
}
borderColor: borderColor,
},
},
},
rowUnselected: {
...rowText
...rowText,
},
rowSelected: {
...rowText,
color: "#201763"
color: "#201763",
},
paginatorContainer: {
display: "flex",
justifyContent: "flex-end",
padding: "5px 38px"
padding: "5px 38px",
},
checkBoxHeader: {
"&.MuiTableCell-paddingCheckbox": {
paddingBottom: 9
}
paddingBottom: 9,
},
},
actionsContainer: {
width: 120,
borderColor: borderColor
borderColor: borderColor,
},
paginatorComponent: {
borderBottom: 0
borderBottom: 0,
},
unCheckedIcon: { ...checkBoxBasic, border: "1px solid #d0d0d0" },
checkedIcon: {
...checkBoxBasic,
border: "1px solid #201763",
backgroundColor: "#201763"
backgroundColor: "#201763",
},
checkBoxRow: {
borderColor: borderColor
}
borderColor: borderColor,
},
});
// Function that renders Title Columns
@@ -221,7 +221,7 @@ const TableWrapper = ({
idField,
classes,
stickyHeader = false,
paginatorConfig
paginatorConfig,
}: TableWrapperProps) => {
return (
<Grid item xs={12}>

View File

@@ -17,40 +17,67 @@
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 {
onChange: (newValue: IElementValue[]) => void;
fields: KVField[];
defaultVals?: IElementValue[];
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
export const valueDef = (
key: string,
type: string,
defaults: IElementValue[]
) => {
let defValue = type === "on|off" ? "false" : "";
if (defaults.length > 0) {
const storedConfig = defaults.find((element) => element.key === key);
if (storedConfig) {
defValue = storedConfig.value;
}
}
return defValue;
};
const ConfTargetGeneric = ({
onChange,
fields,
classes
defaultVals,
classes,
}: IConfGenericProps) => {
const [valueHolder, setValueHolder] = useState<IElementValue[]>([]);
const fieldsElements = !fields ? [] : fields;
const defValList = !defaultVals ? [] : defaultVals;
// 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: field.type === "on|off" ? "false" : ""
value: valueDef(field.name, field.type, defValList),
};
values.push(stateInsert);
});
setValueHolder(values);
}, [fields]);
}, [fields, defaultVals]);
useEffect(() => {
onChange(valueHolder);
@@ -79,7 +106,7 @@ const ConfTargetGeneric = ({
}
selectorOptions={[
{ label: "On", value: "true" },
{ label: "Off", value: "false" }
{ label: "Off", value: "false" },
]}
/>
);
@@ -114,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

@@ -35,38 +35,37 @@ interface IListConfiguration {
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
color: "red",
},
strongText: {
fontWeight: 700
fontWeight: 700,
},
keyName: {
marginLeft: 5
marginLeft: 5,
},
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",
},
iconText: {
lineHeight: "24px"
}
lineHeight: "24px",
},
});
const ConfigurationsList = ({ classes }: IListConfiguration) => {
const [editScreenOpen, setEditScreenOpen] = useState(false);
const [selectedConfiguration, setSelectedConfiguration] = useState({
configuration_id: "",
configuration_label: ""
configuration_label: "",
});
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState("");
const [filter, setFilter] = useState("");
@@ -82,12 +81,12 @@ const ConfigurationsList = ({ classes }: IListConfiguration) => {
setSelectedConfiguration(element);
setEditScreenOpen(true);
}
}
}
},
},
];
const filteredRecords: IConfigurationElement[] = configurationElements.filter(
elementItem =>
(elementItem) =>
elementItem.configuration_id
.toLocaleLowerCase()
.includes(filter.toLocaleLowerCase())
@@ -99,7 +98,6 @@ const ConfigurationsList = ({ classes }: IListConfiguration) => {
<EditConfiguration
open={editScreenOpen}
closeModalAndRefresh={() => {
setIsLoading(true);
setEditScreenOpen(false);
}}
selectedConfiguration={selectedConfiguration}
@@ -119,7 +117,7 @@ const ConfigurationsList = ({ classes }: IListConfiguration) => {
className={classes.searchField}
id="search-resource"
label=""
onChange={event => {
onChange={(event) => {
setFilter(event.target.value);
}}
InputProps={{
@@ -128,7 +126,7 @@ const ConfigurationsList = ({ classes }: IListConfiguration) => {
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
),
}}
/>
</Grid>
@@ -139,9 +137,9 @@ const ConfigurationsList = ({ classes }: IListConfiguration) => {
<TableWrapper
itemActions={tableActions}
columns={[
{ label: "Configuration", elementKey: "configuration_id" }
{ label: "Configuration", elementKey: "configuration_id" },
]}
isLoading={isLoading}
isLoading={false}
records={filteredRecords}
entityName="Configurations"
idField="configuration_id"

View File

@@ -44,29 +44,29 @@ interface IWebhook {
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
color: "red",
},
strongText: {
fontWeight: 700
fontWeight: 700,
},
keyName: {
marginLeft: 5
marginLeft: 5,
},
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",
},
iconText: {
lineHeight: "24px"
}
lineHeight: "24px",
},
});
const panels = {
@@ -77,8 +77,8 @@ const panels = {
apiURL: "",
configuration: {
configuration_id: "logger_webhook",
configuration_label: "Logger Webhook"
}
configuration_label: "Logger Webhook",
},
},
audit: {
main: "audit",
@@ -87,9 +87,9 @@ const panels = {
apiURL: "",
configuration: {
configuration_id: "audit_webhook",
configuration_label: "Audit Webhook"
}
}
configuration_label: "Audit Webhook",
},
},
};
const WebhookPanel = ({ match, classes }: IWebhookPanel) => {
@@ -107,15 +107,15 @@ const WebhookPanel = ({ match, classes }: IWebhookPanel) => {
return null;
}
const filteredRecords: IWebhook[] = webhooks.filter(elementItem =>
const filteredRecords: IWebhook[] = webhooks.filter((elementItem) =>
elementItem.name.toLocaleLowerCase().includes(filter.toLocaleLowerCase())
);
const tableActions = [
{
type: "edit",
onClick: () => {}
}
onClick: () => {},
},
];
return (
@@ -144,7 +144,7 @@ const WebhookPanel = ({ match, classes }: IWebhookPanel) => {
className={classes.searchField}
id="search-resource"
label=""
onChange={event => {
onChange={(event) => {
setFilter(event.target.value);
}}
InputProps={{
@@ -153,7 +153,7 @@ const WebhookPanel = ({ match, classes }: IWebhookPanel) => {
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
),
}}
/>
<Button

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

@@ -15,35 +15,38 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
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 } from "@material-ui/core";
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 {
@@ -59,19 +62,37 @@ const EditConfiguration = ({
closeModalAndRefresh,
serverNeedsRestart,
selectedConfiguration,
classes
classes,
}: IAddNotificationEndpointProps) => {
//Local States
const [valuesObj, setValueObj] = useState<IElementValue[]>([]);
const [saving, setSaving] = useState<boolean>(false);
const [addError, setError] = useState<string>("");
const [loadingConfig, setLoadingConfig] = useState<boolean>(true);
const [errorConfig, setErrorConfig] = useState<string>("");
const [configValues, setConfigValues] = useState<IElementValue[]>([]);
//Effects
useEffect(() => {
const configId = get(selectedConfiguration, "configuration_id", false);
if (configId) {
api
.invoke("GET", `/api/v1/configs/${configId}`)
.then((res) => {
const keyVals = get(res, "key_values", []);
setConfigValues(keyVals);
})
.catch((err) => {
setLoadingConfig(false);
setErrorConfig(err);
});
}
setLoadingConfig(false);
}, [selectedConfiguration]);
useEffect(() => {
if (saving) {
const payload = {
key_values: removeEmptyFields(valuesObj)
key_values: removeEmptyFields(valuesObj),
};
api
.invoke(
@@ -79,16 +100,16 @@ const EditConfiguration = ({
`/api/v1/configs/${selectedConfiguration.configuration_id}`,
payload
)
.then(res => {
.then((res) => {
setSaving(false);
setError("");
setErrorConfig("");
serverNeedsRestart(true);
closeModalAndRefresh();
})
.catch(err => {
.catch((err) => {
setSaving(false);
setError(err);
setErrorConfig(err);
});
}
}, [
@@ -96,7 +117,7 @@ const EditConfiguration = ({
serverNeedsRestart,
selectedConfiguration,
valuesObj,
closeModalAndRefresh
closeModalAndRefresh,
]);
//Fetch Actions
@@ -106,7 +127,7 @@ const EditConfiguration = ({
};
const onValueChange = useCallback(
newValue => {
(newValue) => {
setValueObj(newValue);
},
[setValueObj]
@@ -119,14 +140,14 @@ const EditConfiguration = ({
title={selectedConfiguration.configuration_label}
>
<React.Fragment>
{addError !== "" && (
{errorConfig !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
{errorConfig}
</Typography>
</Grid>
)}
@@ -136,18 +157,23 @@ const EditConfiguration = ({
fieldsConfigurations[selectedConfiguration.configuration_id]
}
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
</Button>
</Grid>
{loadingConfig && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
<Grid item xs={9} />
</form>
</React.Fragment>

View File

@@ -31,36 +31,34 @@ export const configurationElements: IConfigurationElement[] = [
{ configuration_id: "cache", configuration_label: "Cache Configuration" },
{
configuration_id: "compression",
configuration_label: "Compression Configuration"
configuration_label: "Compression Configuration",
},
{ configuration_id: "etcd", configuration_label: "Etcd Configuration" },
{
configuration_id: "identity_openid",
configuration_label: "Identity Openid Configuration"
configuration_label: "Identity Openid Configuration",
},
{
configuration_id: "identity_ldap",
configuration_label: "Identity LDAP Configuration"
configuration_label: "Identity LDAP Configuration",
},
{
configuration_id: "policy_opa",
configuration_label: "Policy OPA Configuration"
configuration_label: "Policy OPA Configuration",
},
{
configuration_id: "kms_vault",
configuration_label: "KMS Vault Configuration"
configuration_label: "KMS Vault Configuration",
},
{ configuration_id: "kms_kes", configuration_label: "KMS KES Configuration" },
{
configuration_id: "logger_webhook",
configuration_label: "Logger Webhook Configuration",
url: "/webhook/logger"
},
{
configuration_id: "audit_webhook",
configuration_label: "Audit Webhook Configuration",
url: "/webhook/audit"
}
},
];
export const fieldsConfigurations: any = {
@@ -70,7 +68,7 @@ export const fieldsConfigurations: any = {
required: true,
label: "name",
tooltip: 'Name of the location of the server e.g. "us-west-rack2"',
type: "string"
type: "string",
},
{
name: "comment",
@@ -78,8 +76,8 @@ export const fieldsConfigurations: any = {
label: "comment",
tooltip: "You can add a comment to this setting",
type: "string",
multiline: true
}
multiline: true,
},
],
cache: [
{
@@ -88,21 +86,21 @@ export const fieldsConfigurations: any = {
label: "Drives",
tooltip:
'Mountpoints e.g. "/optane1" or "/optane2", you can write one per field',
type: "csv"
type: "csv",
},
{
name: "expiry",
required: false,
label: "Expiry",
tooltip: 'Cache expiry duration in days e.g. "90"',
type: "number"
type: "number",
},
{
name: "quota",
required: false,
label: "Quota",
tooltip: 'Limit cache drive usage in percentage e.g. "90"',
type: "number"
type: "number",
},
{
name: "exclude",
@@ -110,14 +108,28 @@ export const fieldsConfigurations: any = {
label: "Exclude",
tooltip:
'Wildcard exclusion patterns e.g. "bucket/*.tmp" or "*.exe", you can write one per field',
type: "csv"
type: "csv",
},
{
name: "after",
required: false,
label: "After",
tooltip: "Minimum number of access before caching an object",
type: "number"
type: "number",
},
{
name: "watermark_low",
required: false,
label: "Watermark Low",
tooltip: "Watermark Low",
type: "number",
},
{
name: "watermark_high",
required: false,
label: "Watermark High",
tooltip: "Watermark High",
type: "number",
},
{
name: "comment",
@@ -125,23 +137,17 @@ export const fieldsConfigurations: any = {
label: "Comment",
tooltip: "You can add a comment to this setting",
type: "string",
multiline: true
}
multiline: true,
},
],
compression: [
{
name: "minio_compress",
required: true,
label: "MinIO Compress",
type: "on|off"
},
{
name: "extensions",
required: false,
label: "Extensions",
tooltip:
'Extensions to compress e.g. ".txt",".log" or ".csv", you can write one per field',
type: "csv"
type: "csv",
},
{
name: "mime_types",
@@ -149,8 +155,8 @@ export const fieldsConfigurations: any = {
label: "Mime Types",
tooltip:
'Mime types e.g. "text/*","application/json" or "application/xml", you can write one per field',
type: "csv"
}
type: "csv",
},
],
etcd: [
{
@@ -159,35 +165,35 @@ export const fieldsConfigurations: any = {
label: "Endpoints",
tooltip:
'List of etcd endpoints e.g. "http://localhost:2379", you can write one per field',
type: "csv"
type: "csv",
},
{
name: "path_prefix",
required: false,
label: "Path Prefix",
tooltip: 'namespace prefix to isolate tenants e.g. "customer1/"',
type: "string"
type: "string",
},
{
name: "coredns_path",
required: false,
label: "Coredns Path",
tooltip: 'Shared bucket DNS records, default is "/skydns"',
type: "string"
type: "string",
},
{
name: "client_cert",
required: false,
label: "Client Cert",
tooltip: "Client cert for mTLS authentication",
type: "string"
type: "string",
},
{
name: "client_cert_key",
required: false,
label: "Client Cert Key",
tooltip: "Client cert key for mTLS authentication",
type: "string"
type: "string",
},
{
name: "comment",
@@ -195,23 +201,37 @@ export const fieldsConfigurations: any = {
label: "Comment",
tooltip: "You can add a comment to this setting",
type: "string",
multiline: true
}
multiline: true,
},
],
identity_openid: [
{
name: "client_id",
required: false,
label: "Client ID",
type: "string"
},
{
name: "config_url",
required: false,
label: "Config URL",
tooltip: "Config URL for Client ID configuration",
type: "string"
}
type: "string",
},
{
name: "client_id",
required: false,
label: "Client ID",
type: "string",
},
{
name: "claim_name",
required: false,
label: "Claim Name",
tooltip: "Claim Name",
type: "string",
},
{
name: "claim_prefix",
required: false,
label: "Claim Prefix",
tooltip: "Claim Prefix",
type: "string",
},
],
identity_ldap: [
{
@@ -219,7 +239,7 @@ export const fieldsConfigurations: any = {
required: true,
label: "Server ADDR",
tooltip: 'AD/LDAP server address e.g. "myldapserver.com:636"',
type: "string"
type: "string",
},
{
name: "username_format",
@@ -227,7 +247,7 @@ export const fieldsConfigurations: any = {
label: "Username Format",
tooltip:
'List of username bind DNs e.g. "uid=%s","cn=accounts","dc=myldapserver" or "dc=com", you can write one per field',
type: "csv"
type: "csv",
},
{
name: "username_search_filter",
@@ -235,7 +255,7 @@ export const fieldsConfigurations: any = {
label: "Username Search Filter",
tooltip:
'User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)"',
type: "string"
type: "string",
},
{
name: "group_search_filter",
@@ -243,21 +263,21 @@ export const fieldsConfigurations: any = {
label: "Group Search Filter",
tooltip:
'Search filter for groups e.g. "(&(objectclass=groupOfNames)(memberUid=%s))"',
type: "string"
type: "string",
},
{
name: "username_search_base_dn",
required: false,
label: "Username Search Base DN",
tooltip: "List of username search DNs, you can write one per field",
type: "csv"
type: "csv",
},
{
name: "group_name_attribute",
required: false,
label: "Group Name Attribute",
tooltip: 'Search attribute for group name e.g. "cn"',
type: "string"
type: "string",
},
{
name: "sts_expiry",
@@ -265,7 +285,7 @@ export const fieldsConfigurations: any = {
label: "STS Expiry",
tooltip:
'temporary credentials validity duration in s,m,h,d. Default is "1h"',
type: "string"
type: "string",
},
{
name: "tls_skip_verify",
@@ -273,7 +293,7 @@ export const fieldsConfigurations: any = {
label: "TLS Skip Verify",
tooltip:
'Trust server TLS without verification, defaults to "off" (verify)',
type: "on|off"
type: "on|off",
},
{
name: "server_insecure",
@@ -281,7 +301,7 @@ export const fieldsConfigurations: any = {
label: "Server Insecure",
tooltip:
'Allow plain text connection to AD/LDAP server, defaults to "off"',
type: "on|off"
type: "on|off",
},
{
name: "comment",
@@ -289,61 +309,59 @@ export const fieldsConfigurations: any = {
label: "Comment",
tooltip: "Optionally add a comment to this setting",
type: "string",
multiline: true
}
multiline: true,
},
],
policy_opa: [
{
name: "opa_url",
name: "url",
required: true,
label: "OPA URL",
type: "string"
}
type: "string",
},
{
name: "auth_token",
required: true,
label: "Auth Token",
type: "string",
},
{
name: "policy_opa",
required: true,
label: "Policy OPA",
type: "string",
},
],
kms_vault: [],
kms_kes: [],
logger_webhook: [
{
name: "name",
name: "endpoint",
required: true,
label: "Name",
tooltip: "Name of the webhook",
type: "string"
label: "Endpoint",
type: "string",
},
{
name: "auth_token",
required: true,
label: "Auth Token",
type: "string"
type: "string",
},
{
name: "endpoint",
required: true,
label: "Endpoint",
type: "string"
}
],
audit_webhook: [
{
name: "name",
name: "endpoint",
required: true,
label: "Name",
tooltip: "Name of the webhook",
type: "string"
label: "Endpoint",
type: "string",
},
{
name: "auth_token",
required: true,
label: "Auth Token",
type: "string"
type: "string",
},
{
name: "endpoint",
required: true,
label: "Endpoint",
type: "string"
}
]
],
};
const commonFields = [
@@ -353,7 +371,7 @@ const commonFields = [
required: true,
tooltip: "staging dir for undelivered messages e.g. '/home/events'",
type: "string"
type: "string",
},
{
name: "queue-limit",
@@ -361,15 +379,15 @@ const commonFields = [
required: false,
tooltip: "maximum limit for undelivered messages, defaults to '10000'",
type: "number"
type: "number",
},
{
name: "comment",
label: "Comment",
required: false,
type: "string",
multiline: true
}
multiline: true,
},
];
export const notificationEndpointsFields: any = {
@@ -380,77 +398,77 @@ export const notificationEndpointsFields: any = {
required: true,
tooltip: "comma separated list of Kafka broker addresses",
type: "string"
type: "string",
},
{
name: "topic",
label: "Topic",
tooltip: "Kafka topic used for bucket notifications",
type: "string"
type: "string",
},
{
name: "sasl_username",
label: "SASL Username",
tooltip: "username for SASL/PLAIN or SASL/SCRAM authentication",
type: "string"
type: "string",
},
{
name: "sasl_password",
label: "SASL Password",
tooltip: "password for SASL/PLAIN or SASL/SCRAM authentication",
type: "string"
type: "string",
},
{
name: "sasl_mechanism",
label: "SASL Mechanism",
tooltip: "sasl authentication mechanism, default 'PLAIN'",
type: "string"
type: "string",
},
{
name: "tls_client_auth",
label: "TLS Client Auth",
tooltip:
"clientAuth determines the Kafka server's policy for TLS client auth",
type: "string"
type: "string",
},
{
name: "sasl",
label: "SASL",
tooltip: "set to 'on' to enable SASL authentication",
type: "on|off"
type: "on|off",
},
{
name: "tls",
label: "TLS",
tooltip: "set to 'on' to enable TLS",
type: "on|off"
type: "on|off",
},
{
name: "tls_skip_verify",
label: "TLS skip verify",
tooltip:
'trust server TLS without verification, defaults to "on" (verify)',
type: "on|off"
type: "on|off",
},
{
name: "client_tls_cert",
label: "client TLS cert",
tooltip: "path to client certificate for mTLS auth",
type: "path"
type: "path",
},
{
name: "client_tls_key",
label: "client TLS key",
tooltip: "path to client key for mTLS auth",
type: "path"
type: "path",
},
{
name: "version",
label: "Version",
tooltip: "specify the version of the Kafka cluster e.g '2.2.0'",
type: "string"
type: "string",
},
...commonFields
...commonFields,
],
[notifyAmqp]: [
{
@@ -459,68 +477,68 @@ export const notificationEndpointsFields: any = {
label: "url",
tooltip:
"AMQP server endpoint e.g. `amqp://myuser:mypassword@localhost:5672`",
type: "url"
type: "url",
},
{
name: "exchange",
label: "exchange",
tooltip: "name of the AMQP exchange",
type: "string"
type: "string",
},
{
name: "exchange_type",
label: "exchange_type",
tooltip: "AMQP exchange type",
type: "string"
type: "string",
},
{
name: "routing_key",
label: "routing_key",
tooltip: "routing key for publishing",
type: "string"
type: "string",
},
{
name: "mandatory",
label: "mandatory",
tooltip:
"quietly ignore undelivered messages when set to 'off', default is 'on'",
type: "on|off"
type: "on|off",
},
{
name: "durable",
label: "durable",
tooltip:
"persist queue across broker restarts when set to 'on', default is 'off'",
type: "on|off"
type: "on|off",
},
{
name: "no_wait",
label: "no_wait",
tooltip:
"non-blocking message delivery when set to 'on', default is 'off'",
type: "on|off"
type: "on|off",
},
{
name: "internal",
label: "internal",
tooltip:
"set to 'on' for exchange to be not used directly by publishers, but only when bound to other exchanges",
type: "on|off"
type: "on|off",
},
{
name: "auto_deleted",
label: "auto_deleted",
tooltip:
"auto delete queue when set to 'on', when there are no consumers",
type: "on|off"
type: "on|off",
},
{
name: "delivery_mode",
label: "delivery_mode",
tooltip: "set to '1' for non-persistent or '2' for persistent queue",
type: "number"
type: "number",
},
...commonFields
...commonFields,
],
[notifyRedis]: [
{
@@ -528,22 +546,22 @@ export const notificationEndpointsFields: any = {
required: true,
label: "address",
tooltip: "Redis server's address. For example: `localhost:6379`",
type: "address"
type: "address",
},
{
name: "key",
required: true,
label: "key",
tooltip: "Redis key to store/update events, key is auto-created",
type: "string"
type: "string",
},
{
name: "password",
label: "password",
tooltip: "Redis server password",
type: "string"
type: "string",
},
...commonFields
...commonFields,
],
[notifyMqtt]: [
{
@@ -551,46 +569,46 @@ export const notificationEndpointsFields: any = {
required: true,
label: "broker",
tooltip: "MQTT server endpoint e.g. `tcp://localhost:1883`",
type: "uri"
type: "uri",
},
{
name: "topic",
required: true,
label: "topic",
tooltip: "name of the MQTT topic to publish",
type: "string"
type: "string",
},
{
name: "username",
label: "username",
tooltip: "MQTT username",
type: "string"
type: "string",
},
{
name: "password",
label: "password",
tooltip: "MQTT password",
type: "string"
type: "string",
},
{
name: "qos",
label: "qos",
tooltip: "set the quality of service priority, defaults to '0'",
type: "number"
type: "number",
},
{
name: "keep_alive_interval",
label: "keep_alive_interval",
tooltip: "keep-alive interval for MQTT connections in s,m,h,d",
type: "duration"
type: "duration",
},
{
name: "reconnect_interval",
label: "reconnect_interval",
tooltip: "reconnect interval for MQTT connections in s,m,h,d",
type: "duration"
type: "duration",
},
...commonFields
...commonFields,
],
[notifyNats]: [
{
@@ -598,95 +616,95 @@ export const notificationEndpointsFields: any = {
required: true,
label: "address",
tooltip: "NATS server address e.g. '0.0.0.0:4222'",
type: "address"
type: "address",
},
{
name: "subject",
required: true,
label: "subject",
tooltip: "NATS subscription subject",
type: "string"
type: "string",
},
{
name: "username",
label: "username",
tooltip: "NATS username",
type: "string"
type: "string",
},
{
name: "password",
label: "password",
tooltip: "NATS password",
type: "string"
type: "string",
},
{
name: "token",
label: "token",
tooltip: "NATS token",
type: "string"
type: "string",
},
{
name: "tls",
label: "tls",
tooltip: "set to 'on' to enable TLS",
type: "on|off"
type: "on|off",
},
{
name: "tls_skip_verify",
label: "tls_skip_verify",
tooltip:
'trust server TLS without verification, defaults to "on" (verify)',
type: "on|off"
type: "on|off",
},
{
name: "ping_interval",
label: "ping_interval",
tooltip: "client ping commands interval in s,m,h,d. Disabled by default",
type: "duration"
type: "duration",
},
{
name: "streaming",
label: "streaming",
tooltip: "set to 'on', to use streaming NATS server",
type: "on|off"
type: "on|off",
},
{
name: "streaming_async",
label: "streaming_async",
tooltip: "set to 'on', to enable asynchronous publish",
type: "on|off"
type: "on|off",
},
{
name: "streaming_max_pub_acks_in_flight",
label: "streaming_max_pub_acks_in_flight",
tooltip: "number of messages to publish without waiting for ACKs",
type: "number"
type: "number",
},
{
name: "streaming_cluster_id",
label: "streaming_cluster_id",
tooltip: "unique ID for NATS streaming cluster",
type: "string"
type: "string",
},
{
name: "cert_authority",
label: "cert_authority",
tooltip: "path to certificate chain of the target NATS server",
type: "string"
type: "string",
},
{
name: "client_cert",
label: "client_cert",
tooltip: "client cert for NATS mTLS auth",
type: "string"
type: "string",
},
{
name: "client_key",
label: "client_key",
tooltip: "client cert key for NATS mTLS auth",
type: "string"
type: "string",
},
...commonFields
...commonFields,
],
[notifyElasticsearch]: [
{
@@ -695,7 +713,7 @@ export const notificationEndpointsFields: any = {
label: "url",
tooltip:
"Elasticsearch server's address, with optional authentication info",
type: "url"
type: "url",
},
{
name: "index",
@@ -703,7 +721,7 @@ export const notificationEndpointsFields: any = {
label: "index",
tooltip:
"Elasticsearch index to store/update events, index is auto-created",
type: "string"
type: "string",
},
{
name: "format",
@@ -711,9 +729,9 @@ export const notificationEndpointsFields: any = {
label: "format",
tooltip:
"'namespace' reflects current bucket/object list and 'access' reflects a journal of object operations, defaults to 'namespace'",
type: "enum"
type: "enum",
},
...commonFields
...commonFields,
],
[notifyWebhooks]: [
{
@@ -722,15 +740,15 @@ export const notificationEndpointsFields: any = {
label: "endpoint",
tooltip:
"webhook server endpoint e.g. http://localhost:8080/minio/events",
type: "url"
type: "url",
},
{
name: "auth_token",
label: "auth_token",
tooltip: "opaque string or JWT authorization token",
type: "string"
type: "string",
},
...commonFields
...commonFields,
],
[notifyNsq]: [
{
@@ -738,34 +756,34 @@ export const notificationEndpointsFields: any = {
required: true,
label: "nsqd_address",
tooltip: "NSQ server address e.g. '127.0.0.1:4150'",
type: "address"
type: "address",
},
{
name: "topic",
required: true,
label: "topic",
tooltip: "NSQ topic",
type: "string"
type: "string",
},
{
name: "tls",
label: "tls",
tooltip: "set to 'on' to enable TLS",
type: "on|off"
type: "on|off",
},
{
name: "tls_skip_verify",
label: "tls_skip_verify",
tooltip:
'trust server TLS without verification, defaults to "on" (verify)',
type: "on|off"
type: "on|off",
},
...commonFields
]
...commonFields,
],
};
export const removeEmptyFields = (formFields: IElementValue[]) => {
const nonEmptyFields = formFields.filter(field => field.value !== "");
const nonEmptyFields = formFields.filter((field) => field.value !== "");
return nonEmptyFields;
};

View File

@@ -14,13 +14,13 @@
// 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 from "react";
import React, { useEffect } from "react";
import clsx from "clsx";
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";
@@ -28,7 +28,6 @@ import Box from "@material-ui/core/Box";
import Typography from "@material-ui/core/Typography";
import Container from "@material-ui/core/Container";
import Link from "@material-ui/core/Link";
import history from "../../history";
import {
Redirect,
@@ -36,14 +35,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";
@@ -62,6 +61,10 @@ import ConfigurationsList from "./Configurations/ConfigurationPanels/Configurati
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";
import { ISessionResponse } from "./types";
import { saveSessionResponse } from "./actions";
function Copyright() {
return (
@@ -81,43 +84,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",
@@ -125,40 +128,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,
@@ -166,22 +169,10 @@ 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
});
const connector = connect(mapState, {
setMenuOpen,
serverNeedsRestart,
serverIsLoading
});
interface IConsoleProps {
open: boolean;
needsRestart: boolean;
@@ -191,141 +182,206 @@ interface IConsoleProps {
setMenuOpen: typeof setMenuOpen;
serverNeedsRestart: typeof serverNeedsRestart;
serverIsLoading: typeof serverIsLoading;
saveSessionResponse: typeof saveSessionResponse;
session: ISessionResponse;
}
class Console extends React.Component<
IConsoleProps & RouteComponentProps & StyledProps & ThemedComponentProps
> {
componentDidMount(): void {
const Console = ({
classes,
open,
needsRestart,
isServerLoading,
serverNeedsRestart,
serverIsLoading,
saveSessionResponse,
session,
}: IConsoleProps) => {
useEffect(() => {
api
.invoke("GET", `/api/v1/session`)
.then(res => {
console.log(res);
.then((res) => {
saveSessionResponse(res);
})
.catch(err => {
.catch((err) => {
storage.removeItem("token");
history.push("/");
});
}
}, [saveSessionResponse]);
restartServer() {
this.props.serverIsLoading(true);
const restartServer = () => {
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);
serverIsLoading(false);
serverNeedsRestart(false);
})
.catch(err => {
this.props.serverIsLoading(false);
.catch((err) => {
serverIsLoading(false);
console.log("failure restarting service");
console.log(err);
});
}
};
render() {
const { classes, open, needsRestart, isServerLoading } = this.props;
return (
<div className={classes.root}>
<CssBaseline />
<Drawer
variant="permanent"
classes={{
paper: clsx(classes.drawerPaper, !open && classes.drawerPaperClose)
}}
open={open}
>
{/*<div className={classes.toolbarIcon}>*/}
{/* <IconButton*/}
{/* onClick={() => {*/}
{/* this.props.setMenuOpen(false);*/}
{/* }}*/}
{/* >*/}
{/* <ChevronLeftIcon />*/}
{/* </IconButton>*/}
{/*</div>*/}
{/*<Divider />*/}
const allowedPages = session.pages.reduce(
(result: any, item: any, index: any) => {
result[item] = true;
return result;
},
{}
);
const routes = [
{
component: Dashboard,
path: "/dashboard",
},
{
component: Buckets,
path: "/buckets",
},
{
component: Buckets,
path: "/buckets/:bucketName",
},
{
component: Watch,
path: "/watch",
},
{
component: Users,
path: "/users",
},
{
component: Groups,
path: "/groups",
},
{
component: Policies,
path: "/policies",
},
{
component: Trace,
path: "/trace",
},
{
component: Logs,
path: "/logs",
},
{
component: ListNotificationEndpoints,
path: "/notification-endpoints",
},
{
component: ConfigurationsList,
path: "/configurations-list",
},
{
component: Permissions,
path: "/permissions",
},
{
component: ServiceAccounts,
path: "/service-accounts",
},
{
component: WebhookPanel,
path: "/webhook/logger",
},
{
component: WebhookPanel,
path: "/webhook/audit",
},
];
const allowedRoutes = routes.filter((route: any) => allowedPages[route.path]);
<Menu />
</Drawer>
return (
<React.Fragment>
{session.status == "ok" ? (
<div className={classes.root}>
<CssBaseline />
<Drawer
variant="permanent"
classes={{
paper: clsx(
classes.drawerPaper,
!open && classes.drawerPaperClose
),
}}
open={open}
>
<Menu pages={session.pages} />
</Drawer>
<main className={classes.content}>
{needsRestart && (
<div className={classes.warningBar}>
{isServerLoading ? (
<React.Fragment>
The server is restarting.
<LinearProgress />
</React.Fragment>
) : (
<React.Fragment>
The instance needs to be restarted for configuration changes
to take effect.{" "}
<Button
color="secondary"
size="small"
onClick={() => {
this.restartServer();
}}
>
Restart
</Button>
</React.Fragment>
)}
</div>
)}
<div className={classes.appBarSpacer} />
<Container maxWidth="lg" className={classes.container}>
<Router history={history}>
<Switch>
<Route path="/buckets" component={Buckets} />
<Route exact path="/permissions" component={Permissions} />
<Route exact path="/policies" component={Policies} />
<Route
exact
path="/service_accounts"
component={ServiceAccounts}
/>
<Route exact path="/users" component={Users} />
<Route exact path="/dashboard" component={Dashboard} />
<Route exct path="/groups" component={Groups} />
<Route
exact
path="/notification-endpoints"
component={ListNotificationEndpoints}
/>
<Route
exact
path="/configurations-list"
component={ConfigurationsList}
/>
<Route exact path="/webhook/logger" component={WebhookPanel} />
<Route exact path="/webhook/audit" component={WebhookPanel} />
<Route exct path="/trace" component={Trace} />
<Route exact path="/">
<Redirect to="/dashboard" />
</Route>
<Route component={NotFoundPage} />
</Switch>
</Router>
<main className={classes.content}>
{needsRestart && (
<div className={classes.warningBar}>
{isServerLoading ? (
<React.Fragment>
The server is restarting.
<LinearProgress />
</React.Fragment>
) : (
<React.Fragment>
The instance needs to be restarted for configuration changes
to take effect.{" "}
<Button
color="secondary"
size="small"
onClick={() => {
restartServer();
}}
>
Restart
</Button>
</React.Fragment>
)}
</div>
)}
<div className={classes.appBarSpacer} />
<Container maxWidth="lg" className={classes.container}>
<Router history={history}>
<Switch>
{allowedRoutes.map((route: any) => (
<Route
key={route.path}
exact
path={route.path}
component={route.component}
/>
))}
{allowedRoutes.length > 0 ? (
<Route exact path="/">
<Redirect to={allowedRoutes[0].path} />
</Route>
) : null}
</Switch>
</Router>
<Box pt={4}>
<Copyright />
</Box>
</Container>
</main>
</div>
);
}
}
<Box pt={4}>
<Copyright />
</Box>
</Container>
</main>
</div>
) : null}
</React.Fragment>
);
};
// );
const mapState = (state: AppState) => ({
open: state.system.sidebarOpen,
needsRestart: state.system.serverNeedsRestart,
isServerLoading: state.system.serverIsLoading,
session: state.console.session,
});
export default withRouter(connector(withStyles(styles)(Console)));
// export default withStyles(styles)(connector(Console));
// export default compose(
// withStyles(styles),
// connector
// )(withRouter(Console))
const connector = connect(mapState, {
setMenuOpen,
serverNeedsRestart,
serverIsLoading,
saveSessionResponse,
});
export default connector(withStyles(styles)(Console));

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

@@ -26,7 +26,7 @@ import { CreateIcon } from "../../../icons";
import api from "../../../common/api";
import { MinTablePaginationActions } from "../../../common/MinTablePaginationActions";
import { GroupsList } from "./types";
import { groupsSort } from "../../../utils/sortFunctions";
import { stringSort } from "../../../utils/sortFunctions";
import AddGroup from "../Groups/AddGroup";
import DeleteGroup from "./DeleteGroup";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
@@ -39,50 +39,50 @@ interface IGroupsProps {
const styles = (theme: Theme) =>
createStyles({
seeMore: {
marginTop: theme.spacing(3)
marginTop: theme.spacing(3),
},
paper: {
// padding: theme.spacing(2),
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),
},
wrapCell: {
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word"
wordWrap: "break-word",
},
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",
},
});
const Groups = ({ classes }: IGroupsProps) => {
@@ -126,7 +126,7 @@ const Groups = ({ classes }: IGroupsProps) => {
.then((res: GroupsList) => {
let resGroups: string[] = [];
if (res.groups !== null) {
resGroups = res.groups.sort(groupsSort);
resGroups = res.groups.sort(stringSort);
}
setRecords(resGroups);
const total = !res.total ? 0 : res.total;
@@ -140,7 +140,7 @@ const Groups = ({ classes }: IGroupsProps) => {
setPage(newPage);
}
})
.catch(err => {
.catch((err) => {
setError(err);
isLoading(false);
});
@@ -162,7 +162,7 @@ const Groups = ({ classes }: IGroupsProps) => {
}
};
const filteredRecords = records.filter(elementItem =>
const filteredRecords = records.filter((elementItem) =>
elementItem.includes(filter)
);
@@ -178,7 +178,7 @@ const Groups = ({ classes }: IGroupsProps) => {
const tableActions = [
{ type: "view", onClick: viewAction },
{ type: "delete", onClick: deleteAction }
{ type: "delete", onClick: deleteAction },
];
return (
@@ -217,9 +217,9 @@ const Groups = ({ classes }: IGroupsProps) => {
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
),
}}
onChange={e => {
onChange={(e) => {
setFilter(e.target.value);
}}
/>
@@ -255,11 +255,11 @@ const Groups = ({ classes }: IGroupsProps) => {
page: page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true
native: true,
},
onChangePage: handleChangePage,
onChangeRowsPerPage: handleChangeRowsPerPage,
ActionsComponent: MinTablePaginationActions
ActionsComponent: MinTablePaginationActions,
}}
/>
</Grid>

View File

@@ -38,43 +38,43 @@ interface IGroupsProps {
const styles = (theme: Theme) =>
createStyles({
seeMore: {
marginTop: theme.spacing(3)
marginTop: theme.spacing(3),
},
paper: {
// padding: theme.spacing(2),
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),
},
wrapCell: {
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word"
wordWrap: "break-word",
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold"
}
}
fontWeight: "bold",
},
},
},
actionsTray: {
textAlign: "left",
"& button": {
marginLeft: 10
}
marginLeft: 10,
},
},
filterField: {
background: "#FFFFFF",
@@ -82,24 +82,24 @@ const styles = (theme: Theme) =>
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012",
width: "100%",
zIndex: 500
zIndex: 500,
},
noFound: {
textAlign: "center",
padding: "10px 0"
padding: "10px 0",
},
tableContainer: {
maxHeight: 250
maxHeight: 250,
},
stickyHeader: {
backgroundColor: "#fff"
}
backgroundColor: "#fff",
},
});
const UsersSelectors = ({
classes,
selectedUsers,
setSelectedUsers
setSelectedUsers,
}: IGroupsProps) => {
//Local States
const [records, setRecords] = useState<any[]>([]);
@@ -133,7 +133,7 @@ const UsersSelectors = ({
elements.push(value);
} else {
// User has unchecked this field, we need to remove it from the list
elements = elements.filter(element => element !== value);
elements = elements.filter((element) => element !== value);
}
setSelectedUsers(elements);
@@ -154,13 +154,13 @@ const UsersSelectors = ({
setError("");
isLoading(false);
})
.catch(err => {
.catch((err) => {
setError(err);
isLoading(false);
});
};
const filteredRecords = records.filter(elementItem =>
const filteredRecords = records.filter((elementItem) =>
elementItem.accessKey.includes(filter)
);
@@ -185,9 +185,9 @@ const UsersSelectors = ({
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
),
}}
onChange={e => {
onChange={(e) => {
setFilter(e.target.value);
}}
/>

View File

@@ -0,0 +1,268 @@
// 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 } from "react";
import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket";
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 { timeFromDate } from "../../../common/utils";
import { isNullOrUndefined } from "util";
import { wsProtocol } from "../../../utils/wsUtils";
const styles = (theme: Theme) =>
createStyles({
logList: {
background: "white",
maxHeight: "400px",
overflow: "auto",
"& ul": {
margin: "4px",
padding: "0px",
},
"& ul li": {
listStyle: "none",
margin: "0px",
padding: "0px",
borderBottom: "1px solid #dedede",
},
},
tab: {
padding: "25px",
},
logerror: {
color: "#A52A2A",
},
logerror_tab: {
color: "#A52A2A",
padding: "25px",
},
ansidefault: {
color: "black",
},
});
interface ILogs {
classes: any;
logMessageReceived: typeof logMessageReceived;
logResetMessages: typeof logResetMessages;
messages: LogMessage[];
}
const Logs = ({
classes,
logMessageReceived,
logResetMessages,
messages,
}: ILogs) => {
useEffect(() => {
logResetMessages();
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/console`
);
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) => {
// console.log(message.data.toString())
let m: LogMessage = JSON.parse(message.data.toString());
m.time = new Date(m.time.toString());
m.key = Math.random();
logMessageReceived(m);
};
c.onclose = () => {
clearInterval(interval);
console.log("connection closed by server");
};
return () => {
c.close(1000);
clearInterval(interval);
console.log("closing websockets");
};
}
}, [logMessageReceived]);
// replaces a character of a string with other at a given index
const replaceWeirdChar = (
origString: string,
replaceChar: string,
index: number
) => {
let firstPart = origString.substr(0, index);
let lastPart = origString.substr(index + 1);
let newString = firstPart + replaceChar + lastPart;
return newString;
};
const renderError = (logElement: LogMessage) => {
let errorElems = [];
if (!isNullOrUndefined(logElement.error)) {
if (logElement.api && logElement.api.name) {
errorElems.push(
<li key={`api-${logElement.key}`}>
<span className={classes.logerror}>API: {logElement.api.name}</span>
</li>
);
}
if (logElement.time) {
errorElems.push(
<li key={`time-${logElement.key}`}>
<span className={classes.logerror}>
Time: {timeFromDate(logElement.time)}
</span>
</li>
);
}
if (logElement.deploymentid) {
errorElems.push(
<li key={`deploytmentid-${logElement.key}`}>
<span className={classes.logerror}>
DeploymentID: {logElement.deploymentid}
</span>
</li>
);
}
if (logElement.requestID) {
errorElems.push(
<li key={`requestid-${logElement.key}`}>
<span className={classes.logerror}>
RequestID: {logElement.requestID}
</span>
</li>
);
}
if (logElement.remotehost) {
errorElems.push(
<li key={`remotehost-${logElement.key}`}>
<span className={classes.logerror}>
RemoteHost: {logElement.remotehost}
</span>
</li>
);
}
if (logElement.host) {
errorElems.push(
<li key={`host-${logElement.key}`}>
<span className={classes.logerror}>Host: {logElement.host}</span>
</li>
);
}
if (logElement.userAgent) {
errorElems.push(
<li key={`useragent-${logElement.key}`}>
<span className={classes.logerror}>
UserAgent: {logElement.userAgent}
</span>
</li>
);
}
if (logElement.error.message) {
errorElems.push(
<li key={`message-${logElement.key}`}>
<span className={classes.logerror}>
Error: {logElement.error.message}
</span>
</li>
);
}
if (logElement.error.source) {
// for all sources add padding
for (let s in logElement.error.source) {
errorElems.push(
<li key={`source-${logElement.key}-${s}`}>
<span className={classes.logerror_tab}>
{logElement.error.source[s]}
</span>
</li>
);
}
}
}
return errorElems;
};
const renderLog = (logElement: LogMessage) => {
let logMessage = logElement.ConsoleMsg;
// remove any non ascii characters, exclude any control codes
logMessage = logMessage.replace(/([^\x20-\x7F])/g, "");
// 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.replace(tColorRegex, "");
// if starts with multiple spaces add padding
if (substr.startsWith(" ")) {
return (
<li key={logElement.key}>
<span className={classes.tab}>{substr}</span>
</li>
);
} else if (!isNullOrUndefined(logElement.error)) {
// list error message and all sources and error elems
return renderError(logElement);
} else {
// for all remaining set default class
return (
<li key={logElement.key}>
<span className={classes.ansidefault}>{substr}</span>
</li>
);
}
};
return (
<div>
<h1>Logs</h1>
<div className={classes.logList}>
<ul>
{messages.map((m) => {
return renderLog(m);
})}
</ul>
</div>
</div>
);
};
const mapState = (state: AppState) => ({
messages: state.logs.messages,
});
const connector = connect(mapState, {
logMessageReceived: logMessageReceived,
logResetMessages: logResetMessages,
});
export default connector(withStyles(styles)(Logs));

View File

@@ -0,0 +1,44 @@
// 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 { LogMessage } from "./types";
export const LOG_MESSAGE_RECEIVED = "LOG_MESSAGE_RECEIVED";
export const LOG_RESET_MESSAGES = "LOG_RESET_MESSAGES";
interface LogMessageReceivedAction {
type: typeof LOG_MESSAGE_RECEIVED;
message: LogMessage;
}
interface LogResetMessagesAction {
type: typeof LOG_RESET_MESSAGES;
}
export type LogActionTypes = LogMessageReceivedAction | LogResetMessagesAction;
export function logMessageReceived(message: LogMessage) {
return {
type: LOG_MESSAGE_RECEIVED,
message: message,
};
}
export function logResetMessages() {
return {
type: LOG_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 {
LOG_MESSAGE_RECEIVED,
LOG_RESET_MESSAGES,
LogActionTypes,
} from "./actions";
import { LogMessage } from "./types";
export interface LogState {
messages: LogMessage[];
}
const initialState: LogState = {
messages: [],
};
export function logReducer(
state = initialState,
action: LogActionTypes
): LogState {
switch (action.type) {
case LOG_MESSAGE_RECEIVED:
return {
...state,
messages: [...state.messages, action.message],
};
case LOG_RESET_MESSAGES:
return {
...state,
messages: [],
};
default:
return state;
}
}

View File

@@ -0,0 +1,44 @@
// 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 logError {
message: string;
source: string[];
}
export interface logErrorApiArgs {
bucket: string;
object: string;
}
export interface logErrorApi {
name: string;
args: logErrorApiArgs;
}
export interface LogMessage {
remotehost: string;
host: string;
requestID: string;
userAgent: string;
message: string;
api: logErrorApi;
deploymentid: string;
time: Date;
error: logError;
ConsoleMsg: string;
key: number;
}

View File

@@ -17,6 +17,9 @@
import React from "react";
import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import RoomServiceIcon from "@material-ui/icons/RoomService";
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";
@@ -32,7 +35,7 @@ import {
BucketsIcon,
DashboardIcon,
PermissionIcon,
UsersIcon
UsersIcon,
} from "../../icons";
import { createStyles, Theme } from "@material-ui/core/styles";
import PersonIcon from "@material-ui/icons/Person";
@@ -48,8 +51,8 @@ const styles = (theme: Theme) =>
marginBottom: "20px",
textAlign: "center",
"& img": {
width: "120px"
}
width: "120px",
},
},
menuList: {
"& .active": {
@@ -59,31 +62,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 });
@@ -91,6 +94,7 @@ const connector = connect(mapState, { userLoggedIn });
interface MenuProps {
classes: any;
userLoggedIn: typeof userLoggedIn;
pages: string[];
}
class Menu extends React.Component<MenuProps> {
@@ -112,65 +116,161 @@ class Menu extends React.Component<MenuProps> {
}
render() {
const { classes } = this.props;
const { classes, pages } = this.props;
const allowedPages = pages.reduce((result: any, item: any, index: any) => {
result[item] = true;
return result;
}, {});
const menu = [
{
type: "item",
component: NavLink,
to: "/dashboard",
name: "Dashboard",
icon: <DashboardIcon />,
},
{
type: "item",
component: NavLink,
to: "/buckets",
name: "Buckets",
icon: <BucketsIcon />,
},
{
type: "item",
component: NavLink,
to: "/service-accounts",
name: "Service Accounts",
icon: <RoomServiceIcon />,
forceDisplay: true,
},
{
type: "item",
component: NavLink,
to: "/watch",
name: "Watch",
icon: <CenterFocusWeakIcon />,
},
{
type: "divider",
key: "divider-1",
},
{
type: "title",
name: "Admin",
component: Typography,
},
{
group: "Admin",
type: "item",
component: NavLink,
to: "/users",
name: "Users",
icon: <PersonIcon />,
},
{
group: "Admin",
type: "item",
component: NavLink,
to: "/groups",
name: "Groups",
icon: <UsersIcon />,
},
{
group: "Admin",
type: "item",
component: NavLink,
to: "/policies",
name: "IAM Policies",
icon: <PermissionIcon />,
},
{
group: "Admin",
type: "item",
component: NavLink,
to: "/trace",
name: "Trace",
icon: <LoopIcon />,
},
{
group: "Admin",
type: "item",
component: NavLink,
to: "/logs",
name: "Console Logs",
icon: <WebAssetIcon />,
},
{
type: "title",
name: "Configuration",
component: Typography,
},
{
group: "Configuration",
type: "item",
component: NavLink,
to: "/notification-endpoints",
name: "Lambda Notifications",
icon: <NotificationsIcon />,
},
{
group: "Configuration",
type: "item",
component: NavLink,
to: "/configurations-list",
name: "Configurations List",
icon: <ListAltIcon />,
},
{
type: "divider",
key: "divider-2",
},
];
const allowedItems = menu.filter(
(item: any) =>
allowedPages[item.to] || item.forceDisplay || item.type !== "item"
);
return (
<React.Fragment>
<div className={classes.logo}>
<img src={logo} alt="logo" />
</div>
<List className={classes.menuList}>
<ListItem button component={NavLink} to="/dashboard">
<ListItemIcon>
<DashboardIcon />
</ListItemIcon>
<ListItemText primary="Dashboard" />
</ListItem>
<ListItem button component={NavLink} to="/buckets">
<ListItemIcon>
<BucketsIcon />
</ListItemIcon>
<ListItemText primary="Buckets" />
</ListItem>
<Divider />
<ListItem component={Typography}>Admin</ListItem>
<ListItem button component={NavLink} to="/users">
<ListItemIcon>
<PersonIcon />
</ListItemIcon>
<ListItemText primary="Users" />
</ListItem>
<ListItem button component={NavLink} to="/groups">
<ListItemIcon>
<UsersIcon />
</ListItemIcon>
<ListItemText primary="Groups" />
</ListItem>
<ListItem button component={NavLink} to="/policies">
<ListItemIcon>
<PermissionIcon />
</ListItemIcon>
<ListItemText primary="IAM Policies" />
</ListItem>
<ListItem button component={NavLink} to="/trace">
<ListItemIcon>
<LoopIcon />
</ListItemIcon>
<ListItemText primary="Trace" />
</ListItem>
<ListItem component={Typography}>Configuration</ListItem>
<ListItem button component={NavLink} to="/notification-endpoints">
<ListItemIcon>
<NotificationsIcon />
</ListItemIcon>
<ListItemText primary="Lambda Notifications" />
</ListItem>
<ListItem button component={NavLink} to="/configurations-list">
<ListItemIcon>
<ListAltIcon />
</ListItemIcon>
<ListItemText primary="Configurations List" />
</ListItem>
<Divider />
{allowedItems.map((page: any) => {
switch (page.type) {
case "divider": {
return <Divider key={page.key} />;
}
case "item": {
return (
<ListItem
key={page.to}
button
component={page.component}
to={page.to}
>
{page.icon && <ListItemIcon>{page.icon}</ListItemIcon>}
{page.name && <ListItemText primary={page.name} />}
</ListItem>
);
}
case "title": {
return (
(allowedItems || []).filter(
(item: any) => item.group === page.name
).length > 0 && (
<ListItem key={page.name} component={page.component}>
{page.name}
</ListItem>
)
);
}
default:
}
})}
<ListItem
button
onClick={() => {

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

@@ -31,14 +31,14 @@ import {
Radio,
RadioGroup,
Select,
TextField
TextField,
} from "@material-ui/core";
import {
createStyles,
lighten,
makeStyles,
Theme,
withStyles
withStyles,
} from "@material-ui/core/styles";
import api from "../../../common/api";
import clsx from "clsx";
@@ -61,21 +61,21 @@ const useToolbarStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(1)
paddingRight: theme.spacing(1),
},
highlight:
theme.palette.type === "light"
? {
color: theme.palette.secondary.main,
backgroundColor: lighten(theme.palette.secondary.light, 0.85)
backgroundColor: lighten(theme.palette.secondary.light, 0.85),
}
: {
color: theme.palette.text.primary,
backgroundColor: theme.palette.secondary.dark
backgroundColor: theme.palette.secondary.dark,
},
title: {
flex: "1 1 100%"
}
flex: "1 1 100%",
},
})
);
@@ -90,7 +90,7 @@ const EnhancedTableToolbar = (props: EnhancedTableToolbarProps) => {
return (
<Toolbar
className={clsx(classes.root, {
[classes.highlight]: numSelected > 0
[classes.highlight]: numSelected > 0,
})}
>
{numSelected > 0 ? (
@@ -122,8 +122,8 @@ const EnhancedTableToolbar = (props: EnhancedTableToolbarProps) => {
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
}
color: "red",
},
});
interface IAddPermissionContentProps {
@@ -164,7 +164,7 @@ class AddPermissionContent extends React.Component<
resources: [],
buckets: [],
bucketsError: "",
loadingBuckets: false
loadingBuckets: false,
};
componentDidMount(): void {
@@ -176,10 +176,10 @@ class AddPermissionContent extends React.Component<
this.setState({
loadingBuckets: false,
buckets: res.buckets,
bucketsError: ""
bucketsError: "",
});
})
.catch(err => {
.catch((err) => {
this.setState({ loadingBuckets: false, bucketsError: err });
});
});
@@ -190,8 +190,8 @@ class AddPermissionContent extends React.Component<
name: selectedPermission.name,
description: selectedPermission.description,
effect: selectedPermission.effect,
resources: selectedPermission.resources.map(r => r.bucket_name),
action: selectedPermission.actions[0].type
resources: selectedPermission.resources.map((r) => r.bucket_name),
action: selectedPermission.actions[0].type,
});
}
}
@@ -204,7 +204,7 @@ class AddPermissionContent extends React.Component<
resources,
description,
effect,
action
action,
} = this.state;
const { selectedPermission } = this.props;
if (addLoading) {
@@ -219,23 +219,23 @@ class AddPermissionContent extends React.Component<
description: description,
effect: effect,
resources: resources,
actions: [action]
actions: [action],
})
.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 {
@@ -245,23 +245,23 @@ class AddPermissionContent extends React.Component<
description: description,
effect: effect,
resources: resources,
actions: [action]
actions: [action],
})
.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,
});
});
}
@@ -280,14 +280,14 @@ class AddPermissionContent extends React.Component<
name,
description,
effect,
action
action,
} = this.state;
const handleSelectAllClick = (
event: React.ChangeEvent<HTMLInputElement>
) => {
if (event.target.checked) {
const newSelecteds = buckets.map(n => n.name);
const newSelecteds = buckets.map((n) => n.name);
this.setState({ resources: newSelecteds });
return;
}
@@ -432,7 +432,7 @@ class AddPermissionContent extends React.Component<
}
onChange={handleSelectAllClick}
inputProps={{
"aria-label": "select all desserts"
"aria-label": "select all desserts",
}}
/>
</TableCell>
@@ -452,7 +452,9 @@ class AddPermissionContent extends React.Component<
return (
<TableRow
hover
onClick={event => handleClick(event, row.name)}
onClick={(event) =>
handleClick(event, row.name)
}
role="checkbox"
aria-checked={isItemSelected}
tabIndex={-1}
@@ -500,7 +502,7 @@ class AddPermissionContent extends React.Component<
value={action}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({
action: (event.target as HTMLInputElement).value
action: (event.target as HTMLInputElement).value,
});
}}
>

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

@@ -1,5 +1,5 @@
// This file is part of MinIO Console Server
// Copyright (c) 2019 MinIO, Inc.
// 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
@@ -14,363 +14,101 @@
// 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 from "react";
import React, { useEffect, useState } from "react";
import Grid from "@material-ui/core/Grid";
import { UnControlled as CodeMirror } from "react-codemirror2";
import Typography from "@material-ui/core/Typography";
import { Button, FormControlLabel, LinearProgress } from "@material-ui/core";
import {
createStyles,
lighten,
makeStyles,
Theme,
withStyles
} from "@material-ui/core/styles";
import api from "../../../common/api";
import clsx from "clsx";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableContainer from "@material-ui/core/TableContainer";
import TableHead from "@material-ui/core/TableHead";
import TablePagination from "@material-ui/core/TablePagination";
import TableRow from "@material-ui/core/TableRow";
import Toolbar from "@material-ui/core/Toolbar";
import Checkbox from "@material-ui/core/Checkbox";
import IconButton from "@material-ui/core/IconButton";
import Tooltip from "@material-ui/core/Tooltip";
import FilterListIcon from "@material-ui/icons/FilterList";
import { Permission, PermissionList } from "../Permissions/types";
import {
NewServiceAccount,
ServiceAccount,
ServiceAccountDetails
} from "./types";
import Switch from "@material-ui/core/Switch";
import { Button, LinearProgress, Tooltip } from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import api from "../../../common/api";
import "codemirror/lib/codemirror.css";
import "codemirror/theme/material.css";
import { NewServiceAccount } from "./types";
import HelpIcon from "@material-ui/icons/Help";
const useToolbarStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(1)
},
highlight:
theme.palette.type === "light"
? {
color: theme.palette.secondary.main,
backgroundColor: lighten(theme.palette.secondary.light, 0.85)
}
: {
color: theme.palette.text.primary,
backgroundColor: theme.palette.secondary.dark
},
title: {
flex: "1 1 100%"
}
})
);
interface EnhancedTableToolbarProps {
numSelected: number;
}
const EnhancedTableToolbar = (props: EnhancedTableToolbarProps) => {
const classes = useToolbarStyles();
const { numSelected } = props;
return (
<Toolbar
className={clsx(classes.root, {
[classes.highlight]: numSelected > 0
})}
>
{numSelected > 0 ? (
<Typography
className={classes.title}
color="inherit"
variant="subtitle1"
>
{numSelected} selected
</Typography>
) : (
<Typography className={classes.title} variant="h6" id="tableTitle">
Permissions
</Typography>
)}
{numSelected > 0 ? (
<span />
) : (
<Tooltip title="Filter list">
<IconButton aria-label="filter list">
<FilterListIcon />
</IconButton>
</Tooltip>
)}
</Toolbar>
);
};
require("codemirror/mode/javascript/javascript");
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
}
color: "red",
},
jsonPolicyEditor: {
minHeight: 400,
width: "100%",
},
codeMirror: {
fontSize: 14,
},
buttonContainer: {
textAlign: "right",
},
...modalBasic,
});
interface IAddServiceAccountContentProps {
interface IAddServiceAccountProps {
classes: any;
open: boolean;
closeModalAndRefresh: (res: NewServiceAccount | null) => void;
selectedServiceAccount: ServiceAccount | null;
}
interface IAddServiceAccountContentState {
addLoading: boolean;
addError: string;
name: string;
enabled: boolean;
selectedPermissions: Permission[];
rowsPerPage: number;
page: number;
permissions: Permission[];
permissionsError: string;
loadingPermissions: boolean;
loadingServiceAccount: boolean;
}
const AddServiceAccount = ({
classes,
open,
closeModalAndRefresh,
}: IAddServiceAccountProps) => {
const [addSending, setAddSending] = useState(false);
const [addError, setAddError] = useState("");
const [policyDefinition, setPolicyDefinition] = useState("");
class AddServiceAccountContent extends React.Component<
IAddServiceAccountContentProps,
IAddServiceAccountContentState
> {
state: IAddServiceAccountContentState = {
addLoading: false,
addError: "",
name: "",
enabled: true,
selectedPermissions: [],
rowsPerPage: 5,
page: 0,
permissions: [],
permissionsError: "",
loadingPermissions: false,
loadingServiceAccount: false
useEffect(() => {
if (addSending) {
api
.invoke("POST", "/api/v1/service-accounts", {
policy: policyDefinition,
})
.then((res) => {
setAddSending(false);
setAddError("");
closeModalAndRefresh(res);
})
.catch((err) => {
setAddSending(false);
setAddError(err);
});
}
}, [
addSending,
setAddSending,
setAddError,
policyDefinition,
closeModalAndRefresh,
]);
const addServiceAccount = (e: React.FormEvent) => {
e.preventDefault();
setAddSending(true);
};
componentDidMount(): void {
// load a list of permissions
this.setState({ loadingPermissions: true }, () => {
api
.invoke("GET", `/api/v1/permissions?limit=1000`)
.then((res: PermissionList) => {
this.setState({
loadingPermissions: false,
permissions: res.permissions,
permissionsError: ""
});
})
.catch(err => {
this.setState({ loadingPermissions: false, permissionsError: err });
});
});
const { selectedServiceAccount } = this.props;
if (selectedServiceAccount !== null) {
this.setState({ loadingServiceAccount: true }, () => {
api
.invoke(
"GET",
`/api/v1/service_accounts/${selectedServiceAccount.id}`
)
.then((res: ServiceAccountDetails) => {
console.log(res);
this.setState({
loadingServiceAccount: false,
name: selectedServiceAccount.name,
enabled: selectedServiceAccount.enabled,
selectedPermissions:
res.permissions === undefined || res.permissions === null
? []
: res.permissions
});
})
.catch(err => {
this.setState({ loadingServiceAccount: false });
});
});
}
}
saveRecord(event: React.FormEvent) {
event.preventDefault();
const { name, addLoading, selectedPermissions, enabled } = this.state;
const { selectedServiceAccount } = this.props;
if (addLoading) {
return;
}
this.setState({ addLoading: true }, () => {
if (selectedServiceAccount !== null) {
api
.invoke(
"PUT",
`/api/v1/service_accounts/${selectedServiceAccount.id}`,
{
id: selectedServiceAccount.id,
name: name,
enabled: enabled,
permission_ids: selectedPermissions.map(p => p.id)
}
)
.then(res => {
this.setState(
{
addLoading: false,
addError: ""
},
() => {
this.props.closeModalAndRefresh(null);
}
);
})
.catch(err => {
this.setState({
addLoading: false,
addError: err
});
});
} else {
api
.invoke("POST", "/api/v1/service_accounts", {
name: name,
permission_ids: selectedPermissions.map(p => p.id)
})
.then((res: NewServiceAccount) => {
this.setState(
{
addLoading: false,
addError: ""
},
() => {
this.props.closeModalAndRefresh(res);
}
);
})
.catch(err => {
this.setState({
addLoading: false,
addError: err
});
});
}
});
}
render() {
const {
classes,
selectedServiceAccount,
open,
closeModalAndRefresh
} = this.props;
const {
addLoading,
addError,
page,
rowsPerPage,
permissions,
selectedPermissions,
name,
loadingServiceAccount
} = this.state;
const handleSelectAllClick = (
event: React.ChangeEvent<HTMLInputElement>
) => {
if (event.target.checked) {
// const newSelecteds = permissions.map(n => n.name);
const newSelecteds = [...permissions];
this.setState({ selectedPermissions: newSelecteds });
return;
}
this.setState({ selectedPermissions: [] });
};
const handleClick = (
event: React.MouseEvent<unknown>,
perm: Permission
) => {
let newSelected: Permission[] = [...selectedPermissions];
if (newSelected.filter(p => p.id === perm.id).length === 0) {
newSelected.push(perm);
} else {
let selectedIndex = -1;
for (let i = 0; i < newSelected.length; i++) {
if (newSelected[i].id === perm.id) {
selectedIndex = i;
break;
}
}
if (selectedIndex >= 0) {
newSelected = [
...newSelected.slice(0, selectedIndex),
...newSelected.slice(selectedIndex + 1)
];
}
}
this.setState({ selectedPermissions: newSelected });
};
const handleChangePage = (event: unknown, newPage: number) => {
this.setState({ page: newPage });
};
const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement>
) => {
this.setState({ page: 0, rowsPerPage: parseInt(event.target.value, 10) });
};
const isSelected = (perm: Permission) =>
selectedPermissions.filter(p => p.id === perm.id).length > 0;
const emptyRows =
rowsPerPage -
Math.min(rowsPerPage, permissions.length - page * rowsPerPage);
const handleChange = (name: string) => (
event: React.ChangeEvent<HTMLInputElement>
) => {
this.setState({ enabled: event.target.checked });
};
return (
<ModalWrapper
modalOpen={open}
onClose={() => {
this.setState({ addError: "" }, () => {
closeModalAndRefresh(null);
});
return (
<ModalWrapper
modalOpen={open}
onClose={() => {
closeModalAndRefresh(null);
}}
title={`Create Service Account`}
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
addServiceAccount(e);
}}
title={
selectedServiceAccount !== null
? "Edit Service Account"
: "Create Service Account"
}
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
this.saveRecord(e);
}}
>
<Grid container>
{loadingServiceAccount && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
<Grid container>
<Grid item xs={12} className={classes.formScrollable}>
{addError !== "" && (
<Grid item xs={12}>
<Typography
@@ -383,155 +121,46 @@ class AddServiceAccountContent extends React.Component<
</Grid>
)}
<Grid item xs={12}>
<InputBoxWrapper
id="service-account-name"
name="service-account-name"
label="Name"
value={name}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ name: e.target.value });
<Typography component="h5">
Optional Policy
<Tooltip
title="A policy that restricts this service account can be attached."
placement="top-start"
>
<HelpIcon />
</Tooltip>
</Typography>
<CodeMirror
className={classes.codeMirror}
options={{
mode: "javascript",
lineNumbers: true,
}}
onChange={(editor, data, value) => {
setPolicyDefinition(value);
}}
/>
</Grid>
<Grid item xs={12}>
<div className={classes.root}>
<EnhancedTableToolbar
numSelected={selectedPermissions.length}
/>
<TableContainer>
<Table
className={classes.table}
aria-labelledby="tableTitle"
size={"small"}
aria-label="enhanced table"
>
<TableHead>
<TableRow>
<TableCell padding="checkbox">
<Checkbox
indeterminate={
selectedPermissions.length > 0 &&
selectedPermissions.length < permissions.length
}
checked={
selectedPermissions.length > 0 &&
selectedPermissions.length === permissions.length
}
onChange={handleSelectAllClick}
inputProps={{
"aria-label": "select all desserts"
}}
/>
</TableCell>
<TableCell>Permission</TableCell>
<TableCell>Description</TableCell>
</TableRow>
</TableHead>
<TableBody>
{permissions
.slice(
page * rowsPerPage,
page * rowsPerPage + rowsPerPage
)
.map((row, index) => {
const isItemSelected = isSelected(row);
const labelId = `enhanced-table-checkbox-${index}`;
return (
<TableRow
hover
onClick={event => handleClick(event, row)}
role="checkbox"
aria-checked={isItemSelected}
tabIndex={-1}
key={row.name}
selected={isItemSelected}
>
<TableCell padding="checkbox">
<Checkbox
checked={isItemSelected}
inputProps={{ "aria-labelledby": labelId }}
/>
</TableCell>
<TableCell id={labelId}>{row.name}</TableCell>
<TableCell>{row.description}</TableCell>
</TableRow>
);
})}
{emptyRows > 0 && (
<TableRow style={{ height: 33 * emptyRows }}>
<TableCell colSpan={6} />
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[5, 10, 25]}
component="div"
count={permissions.length}
rowsPerPage={rowsPerPage}
page={page}
onChangePage={handleChangePage}
onChangeRowsPerPage={handleChangeRowsPerPage}
/>
</div>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<FormControlLabel
control={
<Switch onChange={handleChange("enabled")} value="checkedA" />
}
label="Enabled"
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<Button
type="submit"
variant="contained"
color="primary"
disabled={addLoading}
>
Save
</Button>
</Grid>
{addLoading && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid>
</form>
</ModalWrapper>
);
}
}
<Grid item xs={12} className={classes.buttonContainer}>
<Button
type="submit"
variant="contained"
color="primary"
disabled={addSending}
>
Create
</Button>
</Grid>
{addSending && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid>
</form>
</ModalWrapper>
);
};
const AddServiceAccountWrapper = withStyles(styles)(AddServiceAccountContent);
interface IAddServiceAccountProps {
open: boolean;
closeModalAndRefresh: (res: NewServiceAccount | null) => void;
selectedServiceAccount: ServiceAccount | null;
}
interface IAddServiceAccountState {}
class AddServiceAccount extends React.Component<
IAddServiceAccountProps,
IAddServiceAccountState
> {
state: IAddServiceAccountState = {};
render() {
return <AddServiceAccountWrapper {...this.props} />;
}
}
export default AddServiceAccount;
export default withStyles(styles)(AddServiceAccount);

View File

@@ -1,5 +1,5 @@
// This file is part of MinIO Console Server
// Copyright (c) 2019 MinIO, Inc.
// 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
@@ -17,20 +17,19 @@
import React from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { NewServiceAccount } from "./types";
import {
Button,
DialogActions,
DialogContent,
DialogContentText
} from "@material-ui/core";
import { Button } from "@material-ui/core";
import Typography from "@material-ui/core/Typography";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
import Grid from "@material-ui/core/Grid";
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
}
color: "red",
},
buttonContainer: {
textAlign: "right",
},
});
interface ICredentialsPromptProps {
@@ -40,76 +39,69 @@ interface ICredentialsPromptProps {
closeModal: () => void;
}
interface ICredentialsPromptState {}
const download = (filename: string, text: string) => {
let element = document.createElement("a");
element.setAttribute(
"href",
"data:text/plain;charset=utf-8," + encodeURIComponent(text)
);
element.setAttribute("download", filename);
class CredentialsPrompt extends React.Component<
ICredentialsPromptProps,
ICredentialsPromptState
> {
state: ICredentialsPromptState = {};
element.style.display = "none";
document.body.appendChild(element);
download(filename: string, text: string) {
var element = document.createElement("a");
element.setAttribute(
"href",
"data:text/plain;charset=utf-8," + encodeURIComponent(text)
);
element.setAttribute("download", filename);
element.click();
element.style.display = "none";
document.body.appendChild(element);
document.body.removeChild(element);
};
element.click();
document.body.removeChild(element);
const CredentialsPrompt = ({
classes,
newServiceAccount,
open,
closeModal,
}: ICredentialsPromptProps) => {
if (!newServiceAccount) {
return null;
}
render() {
const { classes, open, newServiceAccount } = this.props;
if (newServiceAccount === null) {
return <div />;
}
return (
<ModalWrapper
modalOpen={open}
onClose={() => {
this.props.closeModal();
}}
title="New Service Account"
>
<React.Fragment>
<DialogContent>
<DialogContentText id="alert-dialog-description">
A new service account has been created with the following details:
<ul>
<li>
<b>Access Key:</b>{" "}
{newServiceAccount.service_account.access_key}
</li>
<li>
<b>Secret Key:</b> {newServiceAccount.secret_key}
</li>
</ul>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
Write these down, as this is the only time the secret will be
displayed.
</Typography>
</DialogContentText>
</DialogContent>
<DialogActions>
return (
<ModalWrapper
modalOpen={open}
onClose={() => {
closeModal();
}}
title="New Service Account Created"
>
<React.Fragment>
<Grid container>
<Grid item xs={12} className={classes.formScrollable}>
A new service account has been created with the following details:
<ul>
<li>
<b>Access Key:</b> {newServiceAccount.accessKey}
</li>
<li>
<b>Secret Key:</b> {newServiceAccount.secretKey}
</li>
</ul>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
Write these down, as this is the only time the secret will be
displayed.
</Typography>
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>
<Button
onClick={() => {
this.download(
download(
"credentials.json",
JSON.stringify({
access_key: newServiceAccount.service_account.access_key,
secret_key: newServiceAccount.secret_key
access_key: newServiceAccount.accessKey,
secret_key: newServiceAccount.secretKey,
})
);
}}
@@ -119,18 +111,18 @@ class CredentialsPrompt extends React.Component<
</Button>
<Button
onClick={() => {
this.props.closeModal();
closeModal();
}}
color="secondary"
autoFocus
>
Done
</Button>
</DialogActions>
</React.Fragment>
</ModalWrapper>
);
}
}
</Grid>
</Grid>
</React.Fragment>
</ModalWrapper>
);
};
export default withStyles(styles)(CredentialsPrompt);

View File

@@ -1,5 +1,5 @@
// This file is part of MinIO Console Server
// Copyright (c) 2019 MinIO, Inc.
// 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
@@ -14,152 +14,117 @@
// 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, { useState, useEffect } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import React from "react";
import Typography from "@material-ui/core/Typography";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress,
} from "@material-ui/core";
import api from "../../../common/api";
import { ServiceAccount, ServiceAccountsList } from "./types";
import Typography from "@material-ui/core/Typography";
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
},
wrapText: {
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word"
}
});
createStyles({
errorBlock: {
color: "red",
},
wrapText: {
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word",
},
});
interface IDeleteServiceAccountProps {
classes: any;
closeDeleteModalAndRefresh: (refresh: boolean) => void;
deleteOpen: boolean;
selectedServiceAccount: ServiceAccount | null;
classes: any;
closeDeleteModalAndRefresh: (refresh: boolean) => void;
deleteOpen: boolean;
selectedServiceAccount: string | null;
}
interface IDeleteServiceAccountState {
deleteLoading: boolean;
deleteError: string;
}
const DeleteServiceAccount = ({
classes,
closeDeleteModalAndRefresh,
deleteOpen,
selectedServiceAccount,
}: IDeleteServiceAccountProps) => {
const [deleteLoading, setDeleteLoading] = useState(false);
const [deleteError, setDeleteError] = useState("");
class DeleteServiceAccount extends React.Component<
IDeleteServiceAccountProps,
IDeleteServiceAccountState
> {
state: IDeleteServiceAccountState = {
deleteLoading: false,
deleteError: ""
};
removeRecord() {
const { deleteLoading } = this.state;
const { selectedServiceAccount } = this.props;
if (deleteLoading) {
return;
}
if (selectedServiceAccount == null) {
return;
}
this.setState({ deleteLoading: true }, () => {
api
.invoke("DELETE", `/api/v1/service_accounts/${selectedServiceAccount.id}`, {
id: selectedServiceAccount.id
})
.then((res: ServiceAccountsList) => {
this.setState(
{
deleteLoading: false,
deleteError: ""
},
() => {
this.props.closeDeleteModalAndRefresh(true);
}
);
})
.catch(err => {
this.setState({
deleteLoading: false,
deleteError: err
});
});
useEffect(() => {
if (deleteLoading) {
api
.invoke("DELETE", `/api/v1/service-accounts/${selectedServiceAccount}`)
.then(() => {
setDeleteLoading(false);
setDeleteError("");
closeDeleteModalAndRefresh(true);
})
.catch((err) => {
setDeleteLoading(false);
setDeleteError(err);
});
}
}, [deleteLoading, closeDeleteModalAndRefresh, selectedServiceAccount]);
render() {
const { classes, deleteOpen, selectedServiceAccount } = this.props;
const { deleteLoading, deleteError } = this.state;
if (selectedServiceAccount === null) {
return <div />;
}
return (
<Dialog
open={deleteOpen}
onClose={() => {
this.setState({ deleteError: "" }, () => {
this.props.closeDeleteModalAndRefresh(false);
});
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Delete ServiceAccount</DialogTitle>
<DialogContent>
{deleteLoading && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
Are you sure you want to delete service account{" "}
<b className={classes.wrapText}>{selectedServiceAccount.name}</b>?
{deleteError !== "" && (
<React.Fragment>
<br />
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{deleteError}
</Typography>
</React.Fragment>
)}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={() => {
this.setState({ deleteError: "" }, () => {
this.props.closeDeleteModalAndRefresh(false);
});
}}
color="primary"
disabled={deleteLoading}
>
Cancel
</Button>
<Button
onClick={() => {
this.removeRecord();
}}
color="secondary"
autoFocus
>
Delete
</Button>
</DialogActions>
</Dialog>
);
const removeRecord = () => {
if (selectedServiceAccount == null) {
return;
}
}
setDeleteLoading(true);
};
return (
<Dialog
open={deleteOpen}
onClose={() => {
closeDeleteModalAndRefresh(false);
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Delete ServiceAccount</DialogTitle>
<DialogContent>
{deleteLoading && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
Are you sure you want to delete service account{" "}
<b className={classes.wrapText}>{selectedServiceAccount}</b>?
{deleteError !== "" && (
<React.Fragment>
<br />
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{deleteError}
</Typography>
</React.Fragment>
)}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={() => {
closeDeleteModalAndRefresh(false);
}}
color="primary"
disabled={deleteLoading}
>
Cancel
</Button>
<Button onClick={removeRecord} color="secondary" autoFocus>
Delete
</Button>
</DialogActions>
</Dialog>
);
};
export default withStyles(styles)(DeleteServiceAccount);

View File

@@ -1,5 +1,5 @@
// This file is part of MinIO Console Server
// Copyright (c) 2019 MinIO, Inc.
// 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
@@ -14,401 +14,295 @@
// 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 from "react";
import React, { useState, useEffect } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import Paper from "@material-ui/core/Paper";
import Grid from "@material-ui/core/Grid";
import api from "../../../common/api";
import {
Button,
IconButton,
LinearProgress,
TableFooter,
TablePagination
} from "@material-ui/core";
import { Button } from "@material-ui/core";
import Typography from "@material-ui/core/Typography";
import {
NewServiceAccount,
ServiceAccount,
ServiceAccountsList
} from "./types";
import { NewServiceAccount } from "./types";
import { MinTablePaginationActions } from "../../../common/MinTablePaginationActions";
import EditIcon from "@material-ui/icons/Edit";
import AddServiceAccount from "./AddServiceAccount";
import DeleteServiceAccount from "./DeleteServiceAccount";
import CredentialsPrompt from "./CredentialsPrompt";
import { CreateIcon, DeleteIcon } from "../../../icons";
import Checkbox from "@material-ui/core/Checkbox";
import PlayArrowRoundedIcon from "@material-ui/icons/PlayArrowRounded";
import { CreateIcon } from "../../../icons";
import TextField from "@material-ui/core/TextField";
import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import { stringSort } from "../../../utils/sortFunctions";
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: "480px",
minWidth: "320px",
padding: "20px"
padding: "20px",
},
errorBlock: {
color: "red"
color: "red",
},
tableToolbar: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(0)
paddingRight: theme.spacing(0),
},
wrapCell: {
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word"
wordWrap: "break-word",
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold"
}
}
fontWeight: "bold",
},
},
},
imageIcon: {
height: "100%"
height: "100%",
},
iconRoot: {
textAlign: "center"
textAlign: "center",
},
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",
},
});
interface IServiceAccountsProps {
classes: any;
}
interface IServiceAccountsState {
records: ServiceAccount[];
totalRecords: number;
loading: boolean;
error: string;
deleteError: string;
addScreenOpen: boolean;
page: number;
rowsPerPage: number;
deleteOpen: boolean;
selectedServiceAccount: ServiceAccount | null;
showNewCredentials: boolean;
newServiceAccount: NewServiceAccount | null;
}
const ServiceAccounts = ({ classes }: IServiceAccountsProps) => {
const [records, setRecords] = useState<string[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>("");
const [filter, setFilter] = useState<string>("");
const [addScreenOpen, setAddScreenOpen] = useState<boolean>(false);
const [page, setPage] = useState<number>(0);
const [rowsPerPage, setRowsPerPage] = useState<number>(10);
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const [selectedServiceAccount, setSelectedServiceAccount] = useState<
string | null
>(null);
const [showNewCredentials, setShowNewCredentials] = useState<boolean>(false);
const [
newServiceAccount,
setNewServiceAccount,
] = useState<NewServiceAccount | null>(null);
class ServiceAccounts extends React.Component<
IServiceAccountsProps,
IServiceAccountsState
> {
state: IServiceAccountsState = {
records: [],
totalRecords: 0,
loading: false,
error: "",
deleteError: "",
addScreenOpen: false,
page: 0,
rowsPerPage: 10,
deleteOpen: false,
selectedServiceAccount: null,
showNewCredentials: false,
newServiceAccount: null
};
useEffect(() => {
fetchRecords();
}, []);
fetchRecords() {
this.setState({ loading: true }, () => {
const { page, rowsPerPage } = this.state;
const offset = page * rowsPerPage;
useEffect(() => {
if (loading) {
api
.invoke(
"GET",
`/api/v1/service_accounts?offset=${offset}&limit=${rowsPerPage}`
)
.then((res: ServiceAccountsList) => {
this.setState({
loading: false,
records: res.service_accounts,
totalRecords: res.total,
error: ""
});
.invoke("GET", `/api/v1/service-accounts`)
.then((res: string[]) => {
const serviceAccounts = res.sort(stringSort);
setLoading(false);
setRecords(serviceAccounts);
setError("");
// if we get 0 results, and page > 0 , go down 1 page
if (
(res.service_accounts === undefined ||
res.service_accounts == null ||
res.service_accounts.length === 0) &&
page > 0
) {
if ((!serviceAccounts || serviceAccounts.length === 0) && page > 0) {
const newPage = page - 1;
this.setState({ page: newPage }, () => {
this.fetchRecords();
});
setPage(newPage);
fetchRecords();
}
})
.catch(err => {
this.setState({ loading: false, error: err });
.catch((err) => {
setError(err);
setLoading(false);
});
});
}
closeAddModalAndRefresh(res: NewServiceAccount | null) {
this.setState({ addScreenOpen: false }, () => {
this.fetchRecords();
});
if (res !== null) {
this.setState({ showNewCredentials: true, newServiceAccount: res });
}
}
}, [loading, setLoading, setRecords, setError, page, setPage]);
closeDeleteModalAndRefresh(refresh: boolean) {
this.setState({ deleteOpen: false }, () => {
if (refresh) {
this.fetchRecords();
}
});
}
const fetchRecords = () => {
setLoading(true);
};
componentDidMount(): void {
this.fetchRecords();
}
const closeAddModalAndRefresh = (res: NewServiceAccount | null) => {
setAddScreenOpen(false);
fetchRecords();
closeCredentialsModal() {
this.setState({ showNewCredentials: false, newServiceAccount: null });
}
if (res !== null) {
setShowNewCredentials(true);
setNewServiceAccount(res);
}
};
render() {
const { classes } = this.props;
const {
records,
totalRecords,
addScreenOpen,
loading,
page,
rowsPerPage,
deleteOpen,
selectedServiceAccount,
showNewCredentials,
newServiceAccount
} = this.state;
const closeDeleteModalAndRefresh = (refresh: boolean) => {
setDeleteOpen(false);
const handleChangePage = (event: unknown, newPage: number) => {
this.setState({ page: newPage }, () => {
this.fetchRecords();
});
};
if (refresh) {
fetchRecords();
}
};
const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement>
) => {
const rPP = parseInt(event.target.value, 10);
this.setState({ page: 0, rowsPerPage: rPP }, () => {
this.fetchRecords();
});
};
const closeCredentialsModal = () => {
setShowNewCredentials(false);
setNewServiceAccount(null);
};
const confirmDeleteServiceAccount = (
selectedServiceAccount: ServiceAccount
) => {
this.setState({
deleteOpen: true,
selectedServiceAccount: selectedServiceAccount
});
};
const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage);
};
const editServiceAccount = (selectedServiceAccount: ServiceAccount) => {
this.setState({
addScreenOpen: true,
selectedServiceAccount: selectedServiceAccount
});
};
const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement>
) => {
const rPP = parseInt(event.target.value, 10);
return (
<React.Fragment>
{addScreenOpen && (
<AddServiceAccount
open={addScreenOpen}
selectedServiceAccount={selectedServiceAccount}
closeModalAndRefresh={(res: NewServiceAccount | null) => {
this.closeAddModalAndRefresh(res);
}}
/>
)}
{deleteOpen && (
<DeleteServiceAccount
deleteOpen={deleteOpen}
selectedServiceAccount={selectedServiceAccount}
closeDeleteModalAndRefresh={(refresh: boolean) => {
this.closeDeleteModalAndRefresh(refresh);
}}
/>
)}
{showNewCredentials && (
<CredentialsPrompt
newServiceAccount={newServiceAccount}
open={showNewCredentials}
closeModal={() => {
this.closeCredentialsModal();
}}
/>
)}
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">Service Accounts</Typography>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Service Accounts"
className={classes.searchField}
id="search-resource"
label=""
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
this.setState({
addScreenOpen: true,
selectedServiceAccount: null
});
}}
>
Create service account
</Button>
<Button
variant="contained"
color="primary"
startIcon={<PlayArrowRoundedIcon />}
onClick={() => {
this.setState({
addScreenOpen: true
});
}}
>
Change Access
</Button>
</Grid>
setPage(0);
setRowsPerPage(rPP);
};
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<Paper className={classes.paper}>
{loading && <LinearProgress />}
{records != null && records.length > 0 ? (
<Table size="medium">
<TableHead className={classes.minTableHeader}>
<TableRow>
<TableCell>Select</TableCell>
<TableCell>Name</TableCell>
<TableCell>Access Key</TableCell>
<TableCell>Status</TableCell>
<TableCell align="right">Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{records.map(row => (
<TableRow key={row.name}>
<TableCell padding="checkbox">
<Checkbox
value="secondary"
color="primary"
inputProps={{ "aria-label": "secondary checkbox" }}
/>
</TableCell>
<TableCell className={classes.wrapCell}>
{row.name}
</TableCell>
<TableCell>{row.access_key}</TableCell>
<TableCell>
{row.enabled ? "enabled" : "disabled"}
</TableCell>
<TableCell align="right">
<IconButton
aria-label="edit"
onClick={() => {
editServiceAccount(row);
}}
>
<EditIcon />
</IconButton>
<IconButton
aria-label="delete"
onClick={() => {
confirmDeleteServiceAccount(row);
}}
>
<DeleteIcon />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={[5, 10, 25]}
colSpan={4}
count={totalRecords}
rowsPerPage={rowsPerPage}
page={page}
SelectProps={{
inputProps: { "aria-label": "rows per page" },
native: true
}}
onChangePage={handleChangePage}
onChangeRowsPerPage={handleChangeRowsPerPage}
ActionsComponent={MinTablePaginationActions}
/>
</TableRow>
</TableFooter>
</Table>
) : (
<div>No Service Accounts</div>
)}
</Paper>
</Grid>
const confirmDeleteServiceAccount = (selectedServiceAccount: string) => {
setSelectedServiceAccount(selectedServiceAccount);
setDeleteOpen(true);
};
const tableActions = [
{ type: "delete", onClick: confirmDeleteServiceAccount },
];
const filteredRecords = records.filter((elementItem) =>
elementItem.toLowerCase().includes(filter.toLowerCase())
);
const beginRecord = page * rowsPerPage;
const endRecords = beginRecord + rowsPerPage;
const paginatedRecords = filteredRecords.slice(beginRecord, endRecords);
return (
<React.Fragment>
{addScreenOpen && (
<AddServiceAccount
open={addScreenOpen}
closeModalAndRefresh={(res: NewServiceAccount | null) => {
closeAddModalAndRefresh(res);
}}
/>
)}
{deleteOpen && (
<DeleteServiceAccount
deleteOpen={deleteOpen}
selectedServiceAccount={selectedServiceAccount}
closeDeleteModalAndRefresh={(refresh: boolean) => {
closeDeleteModalAndRefresh(refresh);
}}
/>
)}
{showNewCredentials && (
<CredentialsPrompt
newServiceAccount={newServiceAccount}
open={showNewCredentials}
closeModal={() => {
closeCredentialsModal();
}}
/>
)}
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">Service Accounts</Typography>
</Grid>
</React.Fragment>
);
}
}
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Service Accounts"
className={classes.searchField}
id="search-resource"
label=""
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
onChange={(e) => {
setFilter(e.target.value);
setPage(0);
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddScreenOpen(true);
setSelectedServiceAccount(null);
}}
>
Create service account
</Button>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
{error !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{error}
</Typography>
</Grid>
)}
<Grid item xs={12}>
<TableWrapper
isLoading={loading}
records={paginatedRecords}
entityName={"Service Accounts"}
idField={""}
columns={[{ label: "Service Account", elementKey: "" }]}
itemActions={tableActions}
paginatorConfig={{
rowsPerPageOptions: [5, 10, 25],
colSpan: 4,
count: records.length,
rowsPerPage: rowsPerPage,
page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true,
},
onChangePage: handleChangePage,
onChangeRowsPerPage: handleChangeRowsPerPage,
ActionsComponent: MinTablePaginationActions,
}}
/>
</Grid>
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(ServiceAccounts);

View File

@@ -1,5 +1,5 @@
// This file is part of MinIO Console Server
// Copyright (c) 2019 MinIO, Inc.
// 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
@@ -14,27 +14,12 @@
// 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 {Permission} from "../Permissions/types";
export interface ServiceAccount {
id: string;
name: string;
slug: string;
access_key: string;
enabled: boolean;
}
export interface ServiceAccountsList {
service_accounts: ServiceAccount[];
total:number;
service_accounts: string[];
total: number;
}
export interface NewServiceAccount {
service_account: ServiceAccount,
secret_key:string,
}
export interface ServiceAccountDetails {
service_account: ServiceAccount,
permissions: Permission[],
accessKey: string;
secretKey: string;
}

View File

@@ -15,13 +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 { traceMessageReceived, traceResetMessages } from "./actions";
import { TraceMessage } from "./types";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { niceBytes, setCookie } from "../../../common/utils";
import { niceBytes, timeFromDate } from "../../../common/utils";
import { wsProtocol } from "../../../utils/wsUtils";
const styles = (theme: Theme) =>
createStyles({
@@ -31,15 +31,15 @@ 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",
},
},
});
interface ITrace {
@@ -53,18 +53,16 @@ const Trace = ({
classes,
traceMessageReceived,
traceResetMessages,
messages
messages,
}: ITrace) => {
useEffect(() => {
traceResetMessages();
const token: string = storage.getItem("token")!;
const url = new URL(window.location.toString());
const isDev = process.env.NODE_ENV === "development";
const port = isDev ? "9090" : url.port;
setCookie("token", token);
const c = new W3CWebSocket(`ws://${url.hostname}:${port}/ws/trace`);
const wsProt = wsProtocol(url.protocol);
const c = new W3CWebSocket(`${wsProt}://${url.hostname}:${port}/ws/trace`);
let interval: any | null = null;
if (c !== null) {
@@ -93,23 +91,15 @@ 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>
<div className={classes.logList}>
<ul>
{messages.map(m => {
{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 + "")}
@@ -123,12 +113,12 @@ const Trace = ({
};
const mapState = (state: AppState) => ({
messages: state.trace.messages
messages: state.trace.messages,
});
const connector = connect(mapState, {
traceMessageReceived: traceMessageReceived,
traceResetMessages: traceResetMessages
traceResetMessages: traceResetMessages,
});
export default connector(withStyles(styles)(Trace));

View File

@@ -35,12 +35,12 @@ export type TraceActionTypes =
export function traceMessageReceived(message: TraceMessage) {
return {
type: TRACE_MESSAGE_RECEIVED,
message: message
message: message,
};
}
export function traceResetMessages() {
return {
type: TRACE_RESET_MESSAGES
type: TRACE_RESET_MESSAGES,
};
}

View File

@@ -17,7 +17,7 @@
import {
TRACE_MESSAGE_RECEIVED,
TRACE_RESET_MESSAGES,
TraceActionTypes
TraceActionTypes,
} from "./actions";
import { TraceMessage } from "./types";
@@ -26,7 +26,7 @@ export interface TraceState {
}
const initialState: TraceState = {
messages: []
messages: [],
};
export function traceReducer(
@@ -37,12 +37,12 @@ export function traceReducer(
case TRACE_MESSAGE_RECEIVED:
return {
...state,
messages: [...state.messages, action.message]
messages: [...state.messages, action.message],
};
case TRACE_RESET_MESSAGES:
return {
...state,
messages: []
messages: [],
};
default:
return state;

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

@@ -24,7 +24,7 @@ import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
import TextField from "@material-ui/core/TextField";
import api from "../../../common/api";
import { groupsSort } from "../../../utils/sortFunctions";
import { stringSort } from "../../../utils/sortFunctions";
import { GroupsList } from "../Groups/types";
import get from "lodash/get";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
@@ -38,43 +38,43 @@ interface IGroupsProps {
const styles = (theme: Theme) =>
createStyles({
seeMore: {
marginTop: theme.spacing(3)
marginTop: theme.spacing(3),
},
paper: {
// padding: theme.spacing(2),
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),
},
wrapCell: {
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word"
wordWrap: "break-word",
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold"
}
}
fontWeight: "bold",
},
},
},
actionsTray: {
textAlign: "left",
"& button": {
marginLeft: 10
}
marginLeft: 10,
},
},
filterField: {
background: "#FFFFFF",
@@ -82,24 +82,24 @@ const styles = (theme: Theme) =>
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012",
width: "100%",
zIndex: 500
zIndex: 500,
},
noFound: {
textAlign: "center",
padding: "10px 0"
padding: "10px 0",
},
tableContainer: {
maxHeight: 200
maxHeight: 200,
},
stickyHeader: {
backgroundColor: "#fff"
}
backgroundColor: "#fff",
},
});
const GroupsSelectors = ({
classes,
selectedGroups,
setSelectedGroups
setSelectedGroups,
}: IGroupsProps) => {
// Local State
const [records, setRecords] = useState<any[]>([]);
@@ -129,11 +129,11 @@ const GroupsSelectors = ({
if (!groups) {
groups = [];
}
setRecords(groups.sort(groupsSort));
setRecords(groups.sort(stringSort));
setError("");
isLoading(false);
})
.catch(err => {
.catch((err) => {
setError(err);
isLoading(false);
});
@@ -151,14 +151,14 @@ const GroupsSelectors = ({
elements.push(value);
} else {
// User has unchecked this field, we need to remove it from the list
elements = elements.filter(element => element !== value);
elements = elements.filter((element) => element !== value);
}
setSelectedGroups(elements);
return elements;
};
const filteredRecords = records.filter(elementItem =>
const filteredRecords = records.filter((elementItem) =>
elementItem.includes(filter)
);
@@ -183,9 +183,9 @@ const GroupsSelectors = ({
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
),
}}
onChange={e => {
onChange={(e) => {
setFilter(e.target.value);
}}
/>

View File

@@ -22,7 +22,7 @@ import {
Grid,
Typography,
TextField,
InputAdornment
InputAdornment,
} from "@material-ui/core";
import SearchIcon from "@material-ui/icons/Search";
import GroupIcon from "@material-ui/icons/Group";
@@ -38,50 +38,50 @@ import TableWrapper from "../Common/TableWrapper/TableWrapper";
const styles = (theme: Theme) =>
createStyles({
seeMore: {
marginTop: theme.spacing(3)
marginTop: theme.spacing(3),
},
paper: {
// padding: theme.spacing(2),
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),
},
wrapCell: {
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word"
wordWrap: "break-word",
},
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",
},
});
interface IUsersProps {
@@ -118,7 +118,7 @@ class Users extends React.Component<IUsersProps, IUsersState> {
selectedUser: null,
addGroupOpen: false,
filter: "",
checkedUsers: []
checkedUsers: [],
};
fetchRecords() {
@@ -133,7 +133,7 @@ class Users extends React.Component<IUsersProps, IUsersState> {
loading: false,
records: users.sort(usersSort),
totalRecords: users.length,
error: ""
error: "",
});
// if we get 0 results, and page > 0 , go down 1 page
if ((!users || users.length === 0) && page > 0) {
@@ -143,7 +143,7 @@ class Users extends React.Component<IUsersProps, IUsersState> {
});
}
})
.catch(err => {
.catch((err) => {
this.setState({ loading: false, error: err });
});
});
@@ -191,28 +191,29 @@ class Users extends React.Component<IUsersProps, IUsersState> {
selectedUser,
filter,
checkedUsers,
addGroupOpen
addGroupOpen,
} = this.state;
const handleChangePage = (event: unknown, newPage: number) => {
this.setState({ page: newPage }, () => {
this.fetchRecords();
});
this.setState({ page: newPage });
};
const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement>
) => {
const rPP = parseInt(event.target.value, 10);
this.setState({ page: 0, rowsPerPage: rPP }, () => {
this.fetchRecords();
});
this.setState({ page: 0, rowsPerPage: rPP });
};
const filteredRecords = records.filter(elementItem =>
const filteredRecords = records.filter((elementItem) =>
elementItem.accessKey.includes(filter)
);
const beginRecord = page * rowsPerPage;
const endRecords = beginRecord + rowsPerPage;
const paginatedRecords = filteredRecords.slice(beginRecord, endRecords);
const selectionChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
const targetD = e.target;
const value = targetD.value;
@@ -225,11 +226,11 @@ class Users extends React.Component<IUsersProps, IUsersState> {
elements.push(value);
} else {
// User has unchecked this field, we need to remove it from the list
elements = elements.filter(element => element !== value);
elements = elements.filter((element) => element !== value);
}
this.setState({
checkedUsers: elements
checkedUsers: elements,
});
return elements;
@@ -238,20 +239,20 @@ class Users extends React.Component<IUsersProps, IUsersState> {
const viewAction = (selectionElement: any): void => {
this.setState({
addScreenOpen: true,
selectedUser: selectionElement
selectedUser: selectionElement,
});
};
const deleteAction = (selectionElement: any): void => {
this.setState({
deleteOpen: true,
selectedUser: selectionElement
selectedUser: selectionElement,
});
};
const tableActions = [
{ type: "view", onClick: viewAction },
{ type: "delete", onClick: deleteAction }
{ type: "delete", onClick: deleteAction },
];
return (
@@ -303,10 +304,10 @@ class Users extends React.Component<IUsersProps, IUsersState> {
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
),
}}
onChange={e => {
this.setState({ filter: e.target.value });
onChange={(e) => {
this.setState({ filter: e.target.value, page: 0 });
}}
/>
<Button
@@ -317,7 +318,7 @@ class Users extends React.Component<IUsersProps, IUsersState> {
onClick={() => {
if (checkedUsers.length > 0) {
this.setState({
addGroupOpen: true
addGroupOpen: true,
});
}
}}
@@ -331,7 +332,7 @@ class Users extends React.Component<IUsersProps, IUsersState> {
onClick={() => {
this.setState({
addScreenOpen: true,
selectedUser: null
selectedUser: null,
});
}}
>
@@ -349,22 +350,22 @@ class Users extends React.Component<IUsersProps, IUsersState> {
onSelect={selectionChanged}
selectedItems={checkedUsers}
isLoading={loading}
records={filteredRecords}
records={paginatedRecords}
entityName="Users"
idField="accessKey"
paginatorConfig={{
rowsPerPageOptions: [5, 10, 25],
colSpan: 3,
count: totalRecords,
count: filteredRecords.length,
rowsPerPage: rowsPerPage,
page: page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true
native: true,
},
onChangePage: handleChangePage,
onChangeRowsPerPage: handleChangeRowsPerPage,
ActionsComponent: MinTablePaginationActions
ActionsComponent: MinTablePaginationActions,
}}
/>
</Grid>

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,46 @@
// 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

@@ -0,0 +1,32 @@
// 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 { ISessionResponse } from "./types";
export const SESSION_RESPONSE = "SESSION_RESPONSE";
interface SessionAction {
type: typeof SESSION_RESPONSE;
message: ISessionResponse;
}
export type SessionActionTypes = SessionAction;
export function saveSessionResponse(message: ISessionResponse) {
return {
type: SESSION_RESPONSE,
message: message,
};
}

View File

@@ -0,0 +1,44 @@
// 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 { ISessionResponse } from "./types";
import { SessionActionTypes, SESSION_RESPONSE } from "./actions";
export interface ConsoleState {
session: ISessionResponse;
}
const initialState: ConsoleState = {
session: {
status: "",
pages: [],
},
};
export function consoleReducer(
state = initialState,
action: SessionActionTypes
): ConsoleState {
switch (action.type) {
case SESSION_RESPONSE:
return {
...state,
session: action.message,
};
default:
return state;
}
}

View File

@@ -0,0 +1,20 @@
// 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 ISessionResponse {
status: string;
pages: string[];
}

View File

@@ -14,23 +14,23 @@
// 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, {FC, useEffect} from "react";
import {RouteComponentProps} from "react-router";
import React, { FC, useEffect } from "react";
import { RouteComponentProps } from "react-router";
import storage from "local-storage-fallback";
import api from "../../common/api";
const LoginCallback: FC<RouteComponentProps> = ({location}) => {
const LoginCallback: FC<RouteComponentProps> = ({ location }) => {
useEffect(() => {
const code = (location.search.match(/code=([^&]+)/) || [])[1];
const state = (location.search.match(/state=([^&]+)/) || [])[1];
api
.invoke("POST", "/api/v1/login/oauth2/auth", {code, state})
.invoke("POST", "/api/v1/login/oauth2/auth", { code, state })
.then((res: any) => {
if (res && res.sessionId) {
// store the jwt token
storage.setItem("token", res.sessionId);
// We push to history the new URL.
window.location.href = "/dashboard";
window.location.href = "/";
}
})
.catch((res: any) => {

View File

@@ -29,6 +29,7 @@ import { userLoggedIn } from "../../actions";
import history from "../../history";
import api from "../../common/api";
import { ILoginDetails } from "./types";
import { setCookie } from "../../common/utils";
const styles = (theme: Theme) =>
createStyles({
@@ -153,6 +154,7 @@ class Login extends React.Component<ILoginProps, ILoginState> {
if (bodyResponse.sessionId) {
// store the jwt token
setCookie("token", bodyResponse.sessionId);
storage.setItem("token", bodyResponse.sessionId);
//return res.body.sessionId;
} else if (bodyResponse.error) {
@@ -163,8 +165,9 @@ class Login extends React.Component<ILoginProps, ILoginState> {
.then(() => {
// We set the state in redux
this.props.userLoggedIn(true);
// We push to history the new URL.
history.push("/dashboard");
// There is a browser cache issue if we change the policy associated to an account and then logout and history.push("/") after login
// therefore after login we need to use window.location redirect
window.location.href = "/";
})
.catch((err) => {
this.setState({ error: `${err}` });

View File

@@ -23,22 +23,21 @@ import Container from "@material-ui/core/Container";
import Copyright from "../common/Copyright";
import history from "../history";
const useStyles = makeStyles(theme => ({
const useStyles = makeStyles((theme) => ({
"@global": {
body: {
backgroundColor: theme.palette.common.white
}
backgroundColor: theme.palette.common.white,
},
},
paper: {
marginTop: theme.spacing(8),
display: "flex",
flexDirection: "column",
alignItems: "center"
}
alignItems: "center",
},
}));
const NotFound: React.FC = () => {
const classes = useStyles();
console.log(history);
return (
<Container component="main">
<CssBaseline />

View File

@@ -18,10 +18,16 @@ import { applyMiddleware, combineReducers, compose, createStore } from "redux";
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";
import { consoleReducer } from "./screens/Console/reducer";
const globalReducer = combineReducers({
system: systemReducer,
trace: traceReducer
trace: traceReducer,
logs: logReducer,
watch: watchReducer,
console: consoleReducer,
});
declare global {

View File

@@ -29,7 +29,7 @@ export const usersSort = (a: userInterface, b: userInterface) => {
return 0;
};
export const groupsSort = (a: string, b: string) => {
export const stringSort = (a: string, b: string) => {
if (a > b) {
return 1;
}

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/>.
export const wsProtocol = (protocol: string): string => {
let wsProtocol = "ws";
if (protocol === "https:") {
wsProtocol = "wss";
}
return wsProtocol;
};

View File

@@ -17,9 +17,10 @@
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react"
"jsx": "react",
"downlevelIteration": true
},
"include": [
"src"
]
}
}

View File

@@ -2027,6 +2027,11 @@ alphanum-sort@^1.0.0:
resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=
anser@^1.4.1:
version "1.4.9"
resolved "https://registry.yarnpkg.com/anser/-/anser-1.4.9.tgz#1f85423a5dcf8da4631a341665ff675b96845760"
integrity sha512-AI+BjTeGt2+WFk4eWcqbQ7snZpDBt8SaLlj0RT2h5xfdWaiy51OjYvqwMrNzJLGy8iOAL6nKDITWO+rd4MkYEA==
ansi-align@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-2.0.0.tgz#c36aeccba563b89ceb556f3690f0b1d9e3547f7f"
@@ -2096,6 +2101,14 @@ ansi-styles@^4.1.0:
"@types/color-name" "^1.1.1"
color-convert "^2.0.1"
ansi-to-react@^6.0.5:
version "6.0.5"
resolved "https://registry.yarnpkg.com/ansi-to-react/-/ansi-to-react-6.0.5.tgz#b41d4e91dbbf847a373368cfa517f337399aef9b"
integrity sha512-kVKsflDGTJP6CnKOrgt7YQGnCurcf4q0rapWTNmx5KcIvgz+wHqjUIkfNmLQcJM6ZeHD0epbSOz0QOLg7fbVmg==
dependencies:
anser "^1.4.1"
escape-carriage "^1.3.0"
ansicolors@~0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979"
@@ -4496,6 +4509,11 @@ es6-symbol@^3.1.1, es6-symbol@~3.1.3:
d "^1.0.1"
ext "^1.1.2"
escape-carriage@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/escape-carriage/-/escape-carriage-1.3.0.tgz#71006b2d4da8cb6828686addafcb094239c742f3"
integrity sha512-ATWi5MD8QlAGQOeMgI8zTp671BG8aKvAC0M7yenlxU4CRLGO/sKthxVUyjiOFKjHdIo+6dZZUNFgHFeVEaKfGQ==
escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"

View File

@@ -28,12 +28,9 @@ import (
"github.com/go-openapi/swag"
"github.com/stretchr/testify/assert"
"github.com/minio/minio/pkg/event/target"
"github.com/minio/minio/cmd/config"
"github.com/minio/mcs/models"
"github.com/minio/minio/cmd/config"
"github.com/minio/minio/pkg/event/target"
"github.com/minio/minio/pkg/madmin"
)
@@ -532,7 +529,7 @@ func Test_getConfig(t *testing.T) {
// mock function response from getConfig()
minioGetConfigKVMock = func(key string) ([]byte, error) {
return nil, errors.New("Invalid config")
return nil, errors.New("invalid config")
}
mockConfigList := madmin.Help{}
@@ -553,7 +550,7 @@ func Test_getConfig(t *testing.T) {
mock: func() {
// mock function response from getConfig()
minioGetConfigKVMock = func(key string) ([]byte, error) {
return nil, errors.New("Invalid config")
return nil, errors.New("invalid config")
}
// mock function response from listConfig()
minioHelpConfigKVMock = func(subSys, key string, envOnly bool) (madmin.Help, error) {

95
restapi/admin_console.go Normal file
View File

@@ -0,0 +1,95 @@
// 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"
"strings"
"time"
"github.com/gorilla/websocket"
"github.com/minio/minio/pkg/madmin"
)
const logTimeFormat string = "15:04:05 MST 01/02/2006"
// startConsoleLog starts log of the servers
func startConsoleLog(ctx context.Context, conn WSConn, client MinioAdmin) error {
// TODO: accept parameters as variables
// name of node, default = "" (all)
node := ""
// number of log lines
lineCount := 100
// type of logs "minio"|"application"|"all" default = "all"
logKind := "all"
// Start listening on all Console Log activity.
logCh := client.getLogs(ctx, node, lineCount, logKind)
for {
select {
case <-ctx.Done():
return nil
case logInfo, ok := <-logCh:
// zero value returned because the channel is closed and empty
if !ok {
return nil
}
if logInfo.Err != nil {
log.Println("error on console logs:", logInfo.Err)
return logInfo.Err
}
// Serialize message to be sent
bytes, err := json.Marshal(serializeConsoleLogInfo(&logInfo))
if err != nil {
fmt.Println("error on json.Marshal:", err)
return err
}
// Send Message through websocket connection
err = conn.writeMessage(websocket.TextMessage, bytes)
if err != nil {
log.Println("error writeMessage:", err)
return err
}
}
}
}
func serializeConsoleLogInfo(l *madmin.LogInfo) (logInfo madmin.LogInfo) {
logInfo = *l
if logInfo.ConsoleMsg != "" {
if strings.HasPrefix(logInfo.ConsoleMsg, "\n") {
logInfo.ConsoleMsg = strings.TrimPrefix(logInfo.ConsoleMsg, "\n")
}
}
if logInfo.Time != "" {
logInfo.Time = getLogTime(logInfo.Time)
}
return logInfo
}
func getLogTime(lt string) string {
tm, err := time.Parse(time.RFC3339Nano, lt)
if err != nil {
return lt
}
return tm.Format(logTimeFormat)
}

View File

@@ -0,0 +1,125 @@
// 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"
"testing"
"github.com/minio/minio/pkg/madmin"
"github.com/stretchr/testify/assert"
)
// assigning mock at runtime instead of compile time
var minioGetLogsMock func(ctx context.Context, node string, lineCnt int, logKind string) <-chan madmin.LogInfo
// mock function of listPolicies()
func (ac adminClientMock) getLogs(ctx context.Context, node string, lineCnt int, logKind string) <-chan madmin.LogInfo {
return minioGetLogsMock(ctx, node, lineCnt, logKind)
}
func TestAdminConsoleLog(t *testing.T) {
assert := assert.New(t)
adminClient := adminClientMock{}
mockWSConn := mockConn{}
function := "startConsoleLog(ctx, )"
ctx := context.Background()
testReceiver := make(chan madmin.LogInfo, 5)
textToReceive := "test message"
testStreamSize := 5
isClosed := false // testReceiver is closed?
// Test-1: Serve Console with no errors until Console finishes sending
// define mock function behavior for minio server Console
minioGetLogsMock = func(ctx context.Context, node string, lineCnt int, logKind string) <-chan madmin.LogInfo {
ch := make(chan madmin.LogInfo)
// Only success, start a routine to start reading line by line.
go func(ch chan<- madmin.LogInfo) {
defer close(ch)
lines := make([]int, testStreamSize)
// mocking sending 5 lines of info
for range lines {
info := madmin.LogInfo{
ConsoleMsg: textToReceive,
}
ch <- info
}
}(ch)
return ch
}
writesCount := 1
// mock connection WriteMessage() no error
connWriteMessageMock = func(messageType int, data []byte) error {
// emulate that receiver gets the message written
var t madmin.LogInfo
_ = json.Unmarshal(data, &t)
if writesCount == testStreamSize {
if !isClosed {
close(testReceiver)
isClosed = true
}
return nil
}
testReceiver <- t
writesCount++
return nil
}
if err := startConsoleLog(ctx, 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.
for i := range testReceiver {
assert.Equal(textToReceive, i.ConsoleMsg)
}
// Test-2: if error happens while writing, return error
connWriteMessageMock = func(messageType int, data []byte) error {
return fmt.Errorf("error on write")
}
if err := startConsoleLog(ctx, mockWSConn, adminClient); assert.Error(err) {
assert.Equal("error on write", err.Error())
}
// Test-3: error happens on GetLogs Minio, Console should stop
// and error shall be returned.
minioGetLogsMock = func(ctx context.Context, node string, lineCnt int, logKind string) <-chan madmin.LogInfo {
ch := make(chan madmin.LogInfo)
// Only success, start a routine to start reading line by line.
go func(ch chan<- madmin.LogInfo) {
defer close(ch)
lines := make([]int, 2)
// mocking sending 5 lines of info
for range lines {
info := madmin.LogInfo{
ConsoleMsg: textToReceive,
}
ch <- info
}
ch <- madmin.LogInfo{Err: fmt.Errorf("error on Console")}
}(ch)
return ch
}
connWriteMessageMock = func(messageType int, data []byte) error {
return nil
}
if err := startConsoleLog(ctx, mockWSConn, adminClient); assert.Error(err) {
assert.Equal("error on Console", err.Error())
}
}

View File

@@ -41,21 +41,21 @@ func registerAdminInfoHandlers(api *operations.McsAPI) {
}
type UsageInfo struct {
type usageInfo struct {
Buckets int64
Objects int64
Usage int64
}
// getAdminInfo invokes admin info and returns a parsed `UsageInfo` structure
func getAdminInfo(ctx context.Context, client MinioAdmin) (*UsageInfo, error) {
// getAdminInfo invokes admin info and returns a parsed `usageInfo` structure
func getAdminInfo(ctx context.Context, client MinioAdmin) (*usageInfo, error) {
serverInfo, err := client.serverInfo(ctx)
if err != nil {
return nil, err
}
// we are trimming uint64 to int64 this will report an incorrect measurement for numbers greater than
// 9,223,372,036,854,775,807
return &UsageInfo{
return &usageInfo{
Buckets: int64(serverInfo.Buckets.Count),
Objects: int64(serverInfo.Objects.Count),
Usage: int64(serverInfo.Usage.Size),

View File

@@ -21,10 +21,8 @@ import (
"encoding/json"
"fmt"
"log"
"net"
"net/http"
"strings"
"sync"
"github.com/gorilla/websocket"
"github.com/minio/minio/pkg/madmin"
@@ -50,125 +48,41 @@ type callStats struct {
Ttfb string `json:"timeToFirstByte"`
}
// trace serves madmin.ServiceTraceInfo
// on a Websocket connection.
func (wsc *wsClient) trace() {
defer func() {
log.Println("trace stopped")
// close connection after return
wsc.conn.close()
}()
log.Println("trace started")
err := startTraceInfo(wsc.conn, wsc.madmin)
// 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, ""))
}
// startTraceInfo starts trace of the servers
// by first 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, sendTraceInfo()
// or wsReadCheck() returns, trace should end.
func startTraceInfo(conn WSConn, client MinioAdmin) (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()
// waitsuntil counter is zero (all are done)
wg.Add(3)
// start go routine for reading websocket heartbeat
readErr := wsReadCheck(ctx, &wg, conn)
// send Stream of Trace Info to the ws c.connection
traceCh := sendTraceInfo(ctx, &wg, conn, client)
// If wsReadCheck returns it means that it is not possible to check
// ws heartbeat anymore so we stop from doing trace, 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)
// wait for traceCh to finish
if err := <-traceCh; err != nil {
mError = err
}
// if traceCh closes for any reason,
// cancel context for all goroutines
cancel()
// wait all goroutines to finish
wg.Wait()
return mError
}
// sendTraceInfo sends stream of Trace Info to the ws connection
func sendTraceInfo(ctx context.Context, wg *sync.WaitGroup, conn WSConn, client MinioAdmin) <-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)
// trace all traffic
allTraffic := true
// Trace failed requests only
errOnly := false
// Start listening on all trace activity.
traceCh := client.serviceTrace(ctx, allTraffic, errOnly)
for traceInfo := range traceCh {
func startTraceInfo(ctx context.Context, conn WSConn, client MinioAdmin) error {
// trace all traffic
allTraffic := true
// Trace failed requests only
errOnly := false
// Start listening on all trace activity.
traceCh := client.serviceTrace(ctx, allTraffic, errOnly)
for {
select {
case <-ctx.Done():
return nil
case traceInfo, ok := <-traceCh:
// zero value returned because the channel is closed and empty
if !ok {
return nil
}
if traceInfo.Err != nil {
log.Println("error on serviceTrace:", traceInfo.Err)
ch <- traceInfo.Err
return
return traceInfo.Err
}
// Serialize message to be sent
traceInfoBytes, err := json.Marshal(shortTrace(&traceInfo))
if err != nil {
fmt.Println("error on json.Marshal:", err)
ch <- err
return
return err
}
// Send Message through websocket connection
err = conn.writeMessage(websocket.TextMessage, traceInfoBytes)
if err != nil {
log.Println("error writeMessage:", err)
ch <- err
return
return err
}
}
// TODO: verbose
}(ch)
return ch
}
}
// shortTrace creates a shorter Trace Info message.

View File

@@ -22,7 +22,6 @@ import (
"fmt"
"testing"
"github.com/gorilla/websocket"
"github.com/minio/minio/pkg/madmin"
trace "github.com/minio/minio/pkg/trace"
"github.com/stretchr/testify/assert"
@@ -40,12 +39,13 @@ func TestAdminTrace(t *testing.T) {
assert := assert.New(t)
adminClient := adminClientMock{}
mockWSConn := mockConn{}
wsClientMock := wsClientMock{madmin: adminClient}
function := "startTraceInfo()"
function := "startTraceInfo(ctx, )"
ctx := context.Background()
testReceiver := make(chan shortTraceMsg, 5)
textToReceive := "test"
testStreamSize := 5
isClosed := false // testReceiver is closed?
// Test-1: Serve Trace with no errors until trace finishes sending
// define mock function behavior for minio server Trace
@@ -65,10 +65,6 @@ func TestAdminTrace(t *testing.T) {
}(ch)
return ch
}
// 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 {
@@ -77,14 +73,17 @@ func TestAdminTrace(t *testing.T) {
_ = json.Unmarshal(data, &t)
if writesCount == testStreamSize {
// for testing we need to close the receiver channel
close(testReceiver)
if !isClosed {
close(testReceiver)
isClosed = true
}
return nil
}
testReceiver <- t
writesCount++
return nil
}
if err := startTraceInfo(mockWSConn, wsClientMock.madmin); err != nil {
if err := startTraceInfo(ctx, 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,50 +95,11 @@ 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(ctx, mockWSConn, adminClient); 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 := startTraceInfo(mockWSConn, wsClientMock.madmin); 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 trace.
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 {
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 := startTraceInfo(mockWSConn, wsClientMock.madmin); 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 := startTraceInfo(mockWSConn, wsClientMock.madmin); assert.Error(err) {
assert.Equal("error on read", err.Error())
}
// Test-7: error happens on serviceTrace Minio, trace should stop
// Test-3: error happens on serviceTrace Minio, trace should stop
// and error shall be returned.
minioServiceTraceMock = func(ctx context.Context, allTrace, errTrace bool) <-chan madmin.ServiceTraceInfo {
ch := make(chan madmin.ServiceTraceInfo)
@@ -158,12 +118,10 @@ func TestAdminTrace(t *testing.T) {
}(ch)
return ch
}
// 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
connWriteMessageMock = func(messageType int, data []byte) error {
return nil
}
if err := startTraceInfo(mockWSConn, wsClientMock.madmin); assert.Error(err) {
if err := startTraceInfo(ctx, 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)
}
@@ -81,8 +83,12 @@ type MinioAdmin interface {
startProfiling(ctx context.Context, profiler madmin.ProfilerType) ([]madmin.StartProfilingResult, error)
stopProfiling(ctx context.Context) (io.ReadCloser, error)
serviceTrace(ctx context.Context, allTrace, errTrace bool) <-chan madmin.ServiceTraceInfo
getLogs(ctx context.Context, node string, lineCnt int, logKind string) <-chan madmin.LogInfo
accountUsageInfo(ctx context.Context) (madmin.AccountUsageInfo, error)
// Service Accounts
addServiceAccount(ctx context.Context, policy *iampolicy.Policy) (mauth.Credentials, error)
listServiceAccounts(ctx context.Context) (madmin.ListServiceAccountsResp, error)
deleteServiceAccount(ctx context.Context, serviceAccount string) error
}
// Interface implementation
@@ -203,23 +209,54 @@ func (ac adminClient) serviceTrace(ctx context.Context, allTrace, errTrace bool)
return ac.client.ServiceTrace(ctx, allTrace, errTrace)
}
// implements madmin.GetLogs()
func (ac adminClient) getLogs(ctx context.Context, node string, lineCnt int, logKind string) <-chan madmin.LogInfo {
return ac.client.GetLogs(ctx, node, lineCnt, logKind)
}
// implements madmin.AddServiceAccount()
func (ac adminClient) addServiceAccount(ctx context.Context, policy *iampolicy.Policy) (mauth.Credentials, error) {
return ac.client.AddServiceAccount(ctx, policy)
}
// implements madmin.ListServiceAccounts()
func (ac adminClient) listServiceAccounts(ctx context.Context) (madmin.ListServiceAccountsResp, error) {
return ac.client.ListServiceAccounts(ctx)
}
// implements madmin.DeleteServiceAccount()
func (ac adminClient) deleteServiceAccount(ctx context.Context, serviceAccount string) error {
return ac.client.DeleteServiceAccount(ctx, serviceAccount)
}
// implements madmin.AccountingUsageInfo()
func (ac adminClient) accountUsageInfo(ctx context.Context) (madmin.AccountUsageInfo, error) {
return ac.client.AccountUsageInfo(ctx)
}
func newMAdminClient(jwt string) (*madmin.AdminClient, error) {
claims, err := auth.JWTAuthenticate(jwt)
if err != nil {
return nil, err
}
adminClient, err := newAdminFromClaims(claims)
if err != nil {
return nil, err
}
return adminClient, nil
}
// 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,7 +115,11 @@ func (c mcS3Client) removeNotificationConfig(arn string, event string, prefix st
return c.client.RemoveNotificationConfig(arn, event, prefix, suffix)
}
// Define MCSCredentials interface with all functions to be implemented
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.
type MCSCredentials interface {
@@ -133,19 +142,77 @@ 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("endpoint cannot be empty for AssumeRoleSTS")
}
if accessKey == "" || secretKey == "" {
return nil, errors.New("creredentials access/secretkey is mandatory for AssumeRoleSTS")
}
// 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
}
}
}
// GetClaimsFromJWT decrypt and returns the claims associated to a provided jwt
func GetClaimsFromJWT(jwt string) (*auth.DecryptedClaims, error) {
claims, err := auth.JWTAuthenticate(jwt)
if err != nil {
return nil, err
}
return claims, nil
}
// getMcsCredentialsFromJWT returns the *minioCredentials.Credentials associated to the
// provided jwt, this is useful for running the Expire() or IsExpired() operations
func getMcsCredentialsFromJWT(jwt string) (*credentials.Credentials, error) {
claims, err := auth.JWTAuthenticate(jwt)
claims, err := GetClaimsFromJWT(jwt)
if err != nil {
return nil, err
}
@@ -160,30 +227,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 +267,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 +281,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

@@ -24,10 +24,19 @@ import (
"github.com/minio/minio/pkg/env"
)
// Port mcs default port
var Port = "9090"
// Hostname mcs hostname
var Hostname = "localhost"
// TLSHostname mcs tls hostname
var TLSHostname = "localhost"
// TLSPort mcs tls port
var TLSPort = "9443"
// TLSRedirect mcs tls redirect rule
var TLSRedirect = "off"
func getAccessKey() string {
@@ -39,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 {
@@ -58,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
}
}
@@ -70,10 +89,14 @@ func getProductionMode() bool {
return strings.ToLower(env.Get(McsProductionMode, "on")) == "on"
}
// GetHostname gets mcs hostname set on env variable,
// default one or defined on run command
func GetHostname() string {
return strings.ToLower(env.Get(McsHostname, Hostname))
}
// GetPort gets mcs por set on env variable
// or default one
func GetPort() int {
port, err := strconv.Atoi(env.Get(McsPort, Port))
if err != nil {
@@ -82,10 +105,14 @@ func GetPort() int {
return port
}
// GetSSLHostname gets mcs ssl hostname set on env variable
// or default one
func GetSSLHostname() string {
return strings.ToLower(env.Get(McsTLSHostname, TLSHostname))
}
// GetSSLPort gets mcs ssl port set on env variable
// or default one
func GetSSLPort() int {
port, err := strconv.Atoi(env.Get(McsTLSPort, TLSPort))
if err != nil {

View File

@@ -27,7 +27,7 @@ import (
"github.com/minio/mcs/models"
"github.com/minio/mcs/pkg/auth"
assetfs "github.com/elazarl/go-bindata-assetfs"
assetFS "github.com/elazarl/go-bindata-assetfs"
portalUI "github.com/minio/mcs/portal-ui"
@@ -167,11 +167,44 @@ func FileServerMiddleware(next http.Handler) http.Handler {
case strings.HasPrefix(r.URL.Path, "/api"):
next.ServeHTTP(w, r)
default:
http.FileServer(&assetfs.AssetFS{
assets := assetFS.AssetFS{
Asset: portalUI.Asset,
AssetDir: portalUI.AssetDir,
AssetInfo: portalUI.AssetInfo,
Prefix: "build"}).ServeHTTP(w, r)
Prefix: "build"}
wrapHandlerSinglePageApplication(http.FileServer(&assets)).ServeHTTP(w, r)
}
})
}
type notFoundRedirectRespWr struct {
http.ResponseWriter // We embed http.ResponseWriter
status int
}
func (w *notFoundRedirectRespWr) WriteHeader(status int) {
w.status = status // Store the status for our own use
if status != http.StatusNotFound {
w.ResponseWriter.WriteHeader(status)
}
}
func (w *notFoundRedirectRespWr) Write(p []byte) (int, error) {
if w.status != http.StatusNotFound {
return w.ResponseWriter.Write(p)
}
return len(p), nil // Lie that we successfully wrote it
}
// wrapHandlerSinglePageApplication handles a http.FileServer returning a 404 and overrides it with index.html
func wrapHandlerSinglePageApplication(h http.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
nfrw := &notFoundRedirectRespWr{ResponseWriter: w}
h.ServeHTTP(nfrw, r)
if nfrw.status == 404 {
log.Printf("Redirecting %s to index.html.", r.RequestURI)
http.Redirect(w, r, "/index.html", http.StatusFound)
}
}
}

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"

View File

@@ -993,6 +993,41 @@ func init() {
}
},
"/service-accounts": {
"get": {
"tags": [
"UserAPI"
],
"summary": "List User's Service Accounts",
"operationId": "ListUserServiceAccounts",
"parameters": [
{
"type": "integer",
"format": "int32",
"name": "offset",
"in": "query"
},
{
"type": "integer",
"format": "int32",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/serviceAccounts"
}
},
"default": {
"description": "Generic error response.",
"schema": {
"$ref": "#/definitions/error"
}
}
}
},
"post": {
"tags": [
"UserAPI"
@@ -1005,7 +1040,7 @@ func init() {
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/serviceAccount"
"$ref": "#/definitions/serviceAccountRequest"
}
}
],
@@ -1025,6 +1060,34 @@ func init() {
}
}
},
"/service-accounts/{access_key}": {
"delete": {
"tags": [
"UserAPI"
],
"summary": "Delete Service Account",
"operationId": "DeleteServiceAccount",
"parameters": [
{
"type": "string",
"name": "access_key",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "A successful response."
},
"default": {
"description": "Generic error response.",
"schema": {
"$ref": "#/definitions/error"
}
}
}
}
},
"/service/restart": {
"post": {
"tags": [
@@ -1883,15 +1946,6 @@ func init() {
}
}
},
"serviceAccount": {
"type": "object",
"properties": {
"policy": {
"type": "string",
"title": "policy to be applied to the Service Account if any"
}
}
},
"serviceAccountCreds": {
"type": "object",
"properties": {
@@ -1903,9 +1957,30 @@ func init() {
}
}
},
"serviceAccountRequest": {
"type": "object",
"properties": {
"policy": {
"type": "string",
"title": "policy to be applied to the Service Account if any"
}
}
},
"serviceAccounts": {
"type": "array",
"items": {
"type": "string"
}
},
"sessionResponse": {
"type": "object",
"properties": {
"pages": {
"type": "array",
"items": {
"type": "string"
}
},
"status": {
"type": "string",
"enum": [
@@ -3033,6 +3108,41 @@ func init() {
}
},
"/service-accounts": {
"get": {
"tags": [
"UserAPI"
],
"summary": "List User's Service Accounts",
"operationId": "ListUserServiceAccounts",
"parameters": [
{
"type": "integer",
"format": "int32",
"name": "offset",
"in": "query"
},
{
"type": "integer",
"format": "int32",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/serviceAccounts"
}
},
"default": {
"description": "Generic error response.",
"schema": {
"$ref": "#/definitions/error"
}
}
}
},
"post": {
"tags": [
"UserAPI"
@@ -3045,7 +3155,7 @@ func init() {
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/serviceAccount"
"$ref": "#/definitions/serviceAccountRequest"
}
}
],
@@ -3065,6 +3175,34 @@ func init() {
}
}
},
"/service-accounts/{access_key}": {
"delete": {
"tags": [
"UserAPI"
],
"summary": "Delete Service Account",
"operationId": "DeleteServiceAccount",
"parameters": [
{
"type": "string",
"name": "access_key",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "A successful response."
},
"default": {
"description": "Generic error response.",
"schema": {
"$ref": "#/definitions/error"
}
}
}
}
},
"/service/restart": {
"post": {
"tags": [
@@ -3923,15 +4061,6 @@ func init() {
}
}
},
"serviceAccount": {
"type": "object",
"properties": {
"policy": {
"type": "string",
"title": "policy to be applied to the Service Account if any"
}
}
},
"serviceAccountCreds": {
"type": "object",
"properties": {
@@ -3943,9 +4072,30 @@ func init() {
}
}
},
"serviceAccountRequest": {
"type": "object",
"properties": {
"policy": {
"type": "string",
"title": "policy to be applied to the Service Account if any"
}
}
},
"serviceAccounts": {
"type": "array",
"items": {
"type": "string"
}
},
"sessionResponse": {
"type": "object",
"properties": {
"pages": {
"type": "array",
"items": {
"type": "string"
}
},
"status": {
"type": "string",
"enum": [

View File

@@ -105,6 +105,9 @@ func NewMcsAPI(spec *loads.Document) *McsAPI {
UserAPIDeleteBucketEventHandler: user_api.DeleteBucketEventHandlerFunc(func(params user_api.DeleteBucketEventParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation user_api.DeleteBucketEvent has not yet been implemented")
}),
UserAPIDeleteServiceAccountHandler: user_api.DeleteServiceAccountHandlerFunc(func(params user_api.DeleteServiceAccountParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation user_api.DeleteServiceAccount has not yet been implemented")
}),
AdminAPIGetUserInfoHandler: admin_api.GetUserInfoHandlerFunc(func(params admin_api.GetUserInfoParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation admin_api.GetUserInfo has not yet been implemented")
}),
@@ -126,6 +129,9 @@ func NewMcsAPI(spec *loads.Document) *McsAPI {
AdminAPIListPoliciesHandler: admin_api.ListPoliciesHandlerFunc(func(params admin_api.ListPoliciesParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation admin_api.ListPolicies has not yet been implemented")
}),
UserAPIListUserServiceAccountsHandler: user_api.ListUserServiceAccountsHandlerFunc(func(params user_api.ListUserServiceAccountsParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation user_api.ListUserServiceAccounts has not yet been implemented")
}),
AdminAPIListUsersHandler: admin_api.ListUsersHandlerFunc(func(params admin_api.ListUsersParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation admin_api.ListUsers has not yet been implemented")
}),
@@ -263,6 +269,8 @@ type McsAPI struct {
UserAPIDeleteBucketHandler user_api.DeleteBucketHandler
// UserAPIDeleteBucketEventHandler sets the operation handler for the delete bucket event operation
UserAPIDeleteBucketEventHandler user_api.DeleteBucketEventHandler
// UserAPIDeleteServiceAccountHandler sets the operation handler for the delete service account operation
UserAPIDeleteServiceAccountHandler user_api.DeleteServiceAccountHandler
// AdminAPIGetUserInfoHandler sets the operation handler for the get user info operation
AdminAPIGetUserInfoHandler admin_api.GetUserInfoHandler
// AdminAPIGroupInfoHandler sets the operation handler for the group info operation
@@ -277,6 +285,8 @@ type McsAPI struct {
AdminAPIListGroupsHandler admin_api.ListGroupsHandler
// AdminAPIListPoliciesHandler sets the operation handler for the list policies operation
AdminAPIListPoliciesHandler admin_api.ListPoliciesHandler
// UserAPIListUserServiceAccountsHandler sets the operation handler for the list user service accounts operation
UserAPIListUserServiceAccountsHandler user_api.ListUserServiceAccountsHandler
// AdminAPIListUsersHandler sets the operation handler for the list users operation
AdminAPIListUsersHandler admin_api.ListUsersHandler
// UserAPILoginHandler sets the operation handler for the login operation
@@ -432,6 +442,9 @@ func (o *McsAPI) Validate() error {
if o.UserAPIDeleteBucketEventHandler == nil {
unregistered = append(unregistered, "user_api.DeleteBucketEventHandler")
}
if o.UserAPIDeleteServiceAccountHandler == nil {
unregistered = append(unregistered, "user_api.DeleteServiceAccountHandler")
}
if o.AdminAPIGetUserInfoHandler == nil {
unregistered = append(unregistered, "admin_api.GetUserInfoHandler")
}
@@ -453,6 +466,9 @@ func (o *McsAPI) Validate() error {
if o.AdminAPIListPoliciesHandler == nil {
unregistered = append(unregistered, "admin_api.ListPoliciesHandler")
}
if o.UserAPIListUserServiceAccountsHandler == nil {
unregistered = append(unregistered, "user_api.ListUserServiceAccountsHandler")
}
if o.AdminAPIListUsersHandler == nil {
unregistered = append(unregistered, "admin_api.ListUsersHandler")
}
@@ -669,6 +685,10 @@ func (o *McsAPI) initHandlerCache() {
o.handlers["DELETE"] = make(map[string]http.Handler)
}
o.handlers["DELETE"]["/buckets/{bucket_name}/events/{arn}"] = user_api.NewDeleteBucketEvent(o.context, o.UserAPIDeleteBucketEventHandler)
if o.handlers["DELETE"] == nil {
o.handlers["DELETE"] = make(map[string]http.Handler)
}
o.handlers["DELETE"]["/service-accounts/{access_key}"] = user_api.NewDeleteServiceAccount(o.context, o.UserAPIDeleteServiceAccountHandler)
if o.handlers["GET"] == nil {
o.handlers["GET"] = make(map[string]http.Handler)
}
@@ -700,6 +720,10 @@ func (o *McsAPI) initHandlerCache() {
if o.handlers["GET"] == nil {
o.handlers["GET"] = make(map[string]http.Handler)
}
o.handlers["GET"]["/service-accounts"] = user_api.NewListUserServiceAccounts(o.context, o.UserAPIListUserServiceAccountsHandler)
if o.handlers["GET"] == nil {
o.handlers["GET"] = make(map[string]http.Handler)
}
o.handlers["GET"]["/users"] = admin_api.NewListUsers(o.context, o.AdminAPIListUsersHandler)
if o.handlers["POST"] == nil {
o.handlers["POST"] = make(map[string]http.Handler)

View File

@@ -53,7 +53,7 @@ type CreateServiceAccountParams struct {
Required: true
In: body
*/
Body *models.ServiceAccount
Body *models.ServiceAccountRequest
}
// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
@@ -67,7 +67,7 @@ func (o *CreateServiceAccountParams) BindRequest(r *http.Request, route *middlew
if runtime.HasBody(r) {
defer r.Body.Close()
var body models.ServiceAccount
var body models.ServiceAccountRequest
if err := route.Consumer.Consume(r.Body, &body); err != nil {
if err == io.EOF {
res = append(res, errors.Required("body", "body"))

View File

@@ -0,0 +1,90 @@
// Code generated by go-swagger; DO NOT EDIT.
// 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 user_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"net/http"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/mcs/models"
)
// DeleteServiceAccountHandlerFunc turns a function with the right signature into a delete service account handler
type DeleteServiceAccountHandlerFunc func(DeleteServiceAccountParams, *models.Principal) middleware.Responder
// Handle executing the request and returning a response
func (fn DeleteServiceAccountHandlerFunc) Handle(params DeleteServiceAccountParams, principal *models.Principal) middleware.Responder {
return fn(params, principal)
}
// DeleteServiceAccountHandler interface for that can handle valid delete service account params
type DeleteServiceAccountHandler interface {
Handle(DeleteServiceAccountParams, *models.Principal) middleware.Responder
}
// NewDeleteServiceAccount creates a new http.Handler for the delete service account operation
func NewDeleteServiceAccount(ctx *middleware.Context, handler DeleteServiceAccountHandler) *DeleteServiceAccount {
return &DeleteServiceAccount{Context: ctx, Handler: handler}
}
/*DeleteServiceAccount swagger:route DELETE /service-accounts/{access_key} UserAPI deleteServiceAccount
Delete Service Account
*/
type DeleteServiceAccount struct {
Context *middleware.Context
Handler DeleteServiceAccountHandler
}
func (o *DeleteServiceAccount) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
route, rCtx, _ := o.Context.RouteInfo(r)
if rCtx != nil {
r = rCtx
}
var Params = NewDeleteServiceAccountParams()
uprinc, aCtx, err := o.Context.Authorize(r, route)
if err != nil {
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
if aCtx != nil {
r = aCtx
}
var principal *models.Principal
if uprinc != nil {
principal = uprinc.(*models.Principal) // this is really a models.Principal, I promise
}
if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
res := o.Handler.Handle(Params, principal) // actually handle the request
o.Context.Respond(rw, r, route.Produces, route, res)
}

View File

@@ -0,0 +1,89 @@
// Code generated by go-swagger; DO NOT EDIT.
// 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 user_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"net/http"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/strfmt"
)
// NewDeleteServiceAccountParams creates a new DeleteServiceAccountParams object
// no default values defined in spec.
func NewDeleteServiceAccountParams() DeleteServiceAccountParams {
return DeleteServiceAccountParams{}
}
// DeleteServiceAccountParams contains all the bound params for the delete service account operation
// typically these are obtained from a http.Request
//
// swagger:parameters DeleteServiceAccount
type DeleteServiceAccountParams struct {
// HTTP Request Object
HTTPRequest *http.Request `json:"-"`
/*
Required: true
In: path
*/
AccessKey string
}
// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
// for simple values it will use straight method calls.
//
// To ensure default values, the struct must have been initialized with NewDeleteServiceAccountParams() beforehand.
func (o *DeleteServiceAccountParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {
var res []error
o.HTTPRequest = r
rAccessKey, rhkAccessKey, _ := route.Params.GetOK("access_key")
if err := o.bindAccessKey(rAccessKey, rhkAccessKey, route.Formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
// bindAccessKey binds and validates parameter AccessKey from path.
func (o *DeleteServiceAccountParams) bindAccessKey(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: true
// Parameter is provided by construction from the route
o.AccessKey = raw
return nil
}

Some files were not shown because too many files have changed in this diff Show More