Compare commits

..

44 Commits

Author SHA1 Message Date
Lenin Alevski
44d8e9b975 idp integration for mcs (#75)
This PR adds support for oidc in  mcs, to enable idp
authentication you need to pass the following environment variables and
restart mcs.

```
MCS_IDP_URL=""
MCS_IDP_CLIENT_ID=""
MCS_IDP_SECRET=""
MCS_IDP_CALLBACK=""
```
2020-05-01 08:38:52 -07:00
Alex
f3d7e61ddb Updated material-ui dependency (#88)
Updated material-ui dependency and fixed a couple os issues that could cause the application to crash
2020-04-30 18:11:34 -07:00
Daniel Valdivia
526c0f4796 UI utils file (#87) 2020-04-30 12:19:33 -07:00
Daniel Valdivia
fe1acaa4b6 Admin Trace UI (#86) 2020-04-30 11:53:50 -07:00
César Nieto
8e9bd8728a Add mcs admin trace api (#82)
Trace Api uses websocket to send trace information, a
valid jwt token needs to be sent either on the header
or as a cookie of the ws request to start.
Three goroutines are needed to ensure communication
if read hearbeat fails all trace should stop by cancelling
the context. WaitGroups are needed to ensure all
goroutines finish gracefully.
2020-04-30 10:50:51 -07:00
Alex
9df9309c66 Configuration List Forms (#83)
Created Lists & forms for configurations in mcs
2020-04-30 00:00:02 -05:00
César Nieto
b85712e29e Add Create Service Account api (#72)
adds new functionality for creating a service
account for a user, for this, an admin client
is created with the user credentials so that
the service account can be assigned to him.

This also updates to  minio RELEASE.2020-04-28T23-56-56Z
2020-04-29 18:28:28 -07:00
Daniel Valdivia
c32df86c76 use target implementation on config api (#81)
Co-authored-by: Cesar Nieto <ces.nietor@gmail.com>
2020-04-29 10:54:59 -07:00
Lenin Alevski
0f52136fd2 STS integration, JWT auth and Stateless MCS (#70)
This commit changes the authentication mechanism between mcs and minio to an sts
(security token service) schema using the user provided credentials, previously
mcs was using master credentials. With that said in order for you to
login to MCS as an admin your user must exists first on minio and have enough
privileges to do administrative operations.

```
./mc admin user add myminio alevsk alevsk12345
```

```
cat admin.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "admin:*",
        "s3:*"
      ],
      "Resource": [
        "arn:aws:s3:::*"
      ]
    }
  ]
}

./mc admin policy add myminio admin admin.json
```

```
./mc admin policy set myminio admin user=alevsk
```
2020-04-22 23:43:17 -07:00
Alex
605b80037a Fixed users list issue (#80)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-04-22 16:40:27 -07:00
Daniel Valdivia
63b430e354 Bug: User List not refreshing after delete (#79) 2020-04-21 17:48:55 -05:00
Alex
d9c212fe2f Tables replacement in mcs (#74)
Replaced all the tables in mcs to be consistent with the new design

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-04-21 15:38:46 -07:00
Daniel Valdivia
5fa0a0fca8 Bug: SSL Mode options for Postgres (#77) 2020-04-21 13:34:15 -07:00
Daniel Valdivia
068ac281ea UI Add Notification Targets (#73) 2020-04-20 20:53:58 -07:00
Alex
0bcf88eb7c Table component implementation (#71)
Implementation of a new table wrapper component that will be used across the application.

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-04-18 08:16:17 -07:00
César Nieto
5c137a8678 Update mcs to latest minio and mc (#69)
updates code to be compatible with:
- github.com/minio/mc v0.0.0-20200415193718-68b638f2f96c
- github.com/minio/minio v0.0.0-20200415191640-bde0f444dbab

Note: admin_config api is patched temporarily now to
return the target configuration as a raw string due to the
changes done on minio.
2020-04-16 13:56:12 -07:00
Alex
540ff31784 Added bulk functionality for add users to groups (#68)
Added functionality in users module to add multiple users to multiple groups at once.

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-04-15 18:08:35 -07:00
Daniel Valdivia
82ea3c1ac4 UI list Lambda Notification Targets (#67) 2020-04-14 14:52:59 -07:00
Alex
e1f177257a Updated form dialog components to be using ModalBoxWrapper component (#66)
Updated all form dialog components in mcs to be using the new ModalBoxWrapper component, This doesn't affect delete dialogs since we are going to create an independent component for those.

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-04-13 22:27:34 -07:00
Alex
0211827c74 Removed console messages in mcs-users module (#65)
Removed console messages in mcs-users module
2020-04-13 17:06:43 -07:00
Daniel Valdivia
ac5732970c New Logo (#64) 2020-04-13 10:59:46 -07:00
Alex
1b1ed55252 Creation of reusable components for mcs & implementation in users page (#63)
Creation of reusable componentes for mcs:
- ModalWrapper => Modal box component with MinIO styles
- InputBoxWrapper => Input box component with MinIO styles
- RadioGroupSelector => Component that generates a Radio Group Selector combo with the requested options and MinIO styles

Implementation of these new components in users creation / edit components

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-04-10 22:13:31 -07:00
Alex
5c5e84b289 Implemented User-Groups integration for mcs (#62)
Implemented user-groups integration for mcs, this allows to store the user groups during the user creation.

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-04-09 16:39:49 -07:00
Daniel Valdivia
5755b98b66 API to list and add Notification Endpoints (#50) 2020-04-09 16:07:26 -07:00
Harshavardhana
86ee1eea6d add code of conduct, contributing and security guidelines (#61) 2020-04-09 12:04:15 -07:00
Harshavardhana
adcbf61049 add support for mcs build to trim gopaths (#60)
also remove `version` sub-command as we don't
use this anymore, just use `mcs --version`
2020-04-09 11:29:49 -07:00
Alex
e197399441 Users-Group Update API (#49)
* Added structure to swagger

* Added updateUserGroups handlers

* Updated return definition for user groups.

* Logic rewrite

* Removed logs

* Added some tests to updateUserGroups

* lint fix

* Updated tests for the new API

* Lint

* Added comment about why we are setting this groups individually. & more lint fixes

* Updated tests page

* Added more tests & fixed comments for PR

* Lint utils file

* Fixed import orders

* Changed import order

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-04-08 17:38:18 -07:00
Lenin Alevski
ff2438a877 Logout endpoint (#47)
Delete in memory session when user logout from mcs

lint fixes

Click logout button triggers logout request

Clicking the actual logout button send the POST /logout request on mcs
UI

Co-authored-by: Daniel Valdivia <hola@danielvaldivia.com>
2020-04-08 14:36:14 -05:00
César Nieto
c85067dfba UI remove setting access on make bucket (#46)
Remove CUSTOM option on access to set
2020-04-08 07:44:32 -07:00
Daniel Valdivia
b1df170d80 Change font to Lato (#48) 2020-04-07 17:31:26 -07:00
Daniel Valdivia
9566f6e579 Fix Groups Empty (#45) 2020-04-07 13:29:40 -07:00
Daniel Valdivia
12a682e9f6 Redirect to Login when not logged in. (#44) 2020-04-07 11:56:52 -07:00
César Nieto
e0bb098e47 mcs delete bucket event notification api (#36)
Co-authored-by: Lenin Alevski <alevsk.8772@gmail.com>
2020-04-07 09:27:25 -07:00
Lenin Alevski
b390ce309a Reading policy as json string (#43)
addPolicy endpoint will read policies as json string, this to allow
s3 iam policy compatibility (uppercase in json attributes) and to be
consistent with other mcs apis, once https://github.com/minio/minio/pull/9181
is merged we can return a type struct{}

fix policies test to new refactor

goimports

more golint fixes
2020-04-06 19:10:10 -07:00
Alex
3dac86d3ce Implements remove user API (#42)
* Implementation of RemoveUser from madmin

* Added removeUser structure.

* Added removeUserResponse actions

* Added delete API to swagger

* Added tests to removeUser functions

* Removed extra space at EOF

* Changed context to be a parameter in admin_users functions

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-04-06 18:07:32 -07:00
Daniel Valdivia
2001ab6dae Logout on Unauthorized. Fix all UI warnings. (#35) 2020-04-06 16:35:30 -07:00
César Nieto
31f0655ff6 mcs add tests for groups and mock same function twice (#41) 2020-04-06 16:18:28 -07:00
César Nieto
775874cf86 update golangci-lint to v1.24 on github workflow (#40)
* update golangci-lint to v1.24 on github workflow

* fix lint errors
2020-04-06 16:04:18 -07:00
Lenin Alevski
9ca4daa906 TLS redirect enabled by default (#39)
When certificates are provided to mcs, tls direct will be
enabled by default (http://localhost -> https:localhost), you
can change this behavior by providing the `MCS_SECURE_SSL_REDIRECT=off`
env variable
2020-04-06 15:59:21 -07:00
Lenin Alevski
2318a8a82b disabling default tls redirect (#38)
Co-authored-by: César Nieto <ces.nietor@gmail.com>
2020-04-06 15:22:39 -07:00
Daniel Valdivia
69ed6f5ca4 Development md (#34)
* Introduce DEVELOPMENT.md

* Ignore *.orig files

Co-authored-by: Lenin Alevski <alevsk.8772@gmail.com>
2020-04-06 14:22:27 -07:00
Lenin Alevski
3a96e6d7e7 Secure Middleware (#37)
adding secure middleware to enforce security headers, most
of the options can be configured via env variables

adding prefix for mcs env variables

adding http redirect to https, adding csp report only, etc

solving conflicts

passing tls port configured by cli to secure middleware

update go.sum

adding default port, tlsport, host and tlshostname

fix tlsport bug
2020-04-06 13:24:15 -07:00
Daniel Valdivia
9a2b10476c Updated README.md (#33) 2020-04-06 12:07:40 -07:00
Daniel Valdivia
c8938dc131 Fix Module (#32) 2020-04-06 11:58:34 -07:00
290 changed files with 21216 additions and 3606 deletions

View File

@@ -31,7 +31,7 @@ jobs:
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.21.0
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 ./...

9
.gitignore vendored
View File

@@ -6,6 +6,7 @@
*.dylib
.DS_Store
*.swp
*.orig
# Test binary, build with `go test -c`
*.test
@@ -22,3 +23,11 @@ mcs
!mcs/
dist/
# Ignore tls cert and key
private.key
public.crt
# Ignore VsCode files
.vscode/
*.code-workspace

59
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,59 @@
# MinIO Console Server Contribution Guide [![Slack](https://slack.min.io/slack?type=svg)](https://slack.min.io)
This is a REST portal server created using [go-swagger](https://github.com/go-swagger/go-swagger)
The API handlers are created using a YAML definition located in `swagger.YAML`.
To add new api, the YAML file needs to be updated with all the desired apis using the [Swagger Basic Structure](https://swagger.io/docs/specification/2-0/basic-structure/), this includes paths, parameters, definitions, tags, etc.
## Generate server from YAML
Once the YAML file is ready we can autogenerate the code needed for the new api by just running:
Validate it:
```
swagger validate ./swagger.yml
```
Update server code:
```
make swagger-gen
```
This will update all the necessary code.
`./restapi/configure_mcs.go` is a file that contains the handlers to be used by the application, here is the only place where we need to update our code to support the new apis. This file is not affected when running the swagger generator and it is safe to edit.
## Unit Tests
`./restapi/handlers_test.go` needs to be updated with the proper tests for the new api.
To run tests:
```
go test ./restapi
```
## Commit changes
After verification, commit your changes. This is a [great post](https://chris.beams.io/posts/git-commit/) on how to write useful commit messages
```
$ git commit -am 'Add some feature'
```
### Push to the branch
Push your locally committed changes to the remote origin (your fork)
```
$ git push origin my-new-feature
```
### Create a Pull Request
Pull requests can be created via GitHub. Refer to [this document](https://help.github.com/articles/creating-a-pull-request/) for detailed steps on how to create a pull request. After a Pull Request gets peer reviewed and approved, it will be merged.
## FAQs
### How does ``mcs`` manages dependencies?
``MinIO`` uses `go mod` to manage its dependencies.
- Run `go get foo/bar` in the source folder to add the dependency to `go.mod` file.
To remove a dependency
- Edit your code and remove the import reference.
- Run `go mod tidy` in the source folder to remove dependency from `go.mod` file.
### What are the coding guidelines for mcs?
``mcs`` is fully conformant with Golang style. Refer: [Effective Go](https://github.com/golang/go/wiki/CodeReviewComments) article from Golang project. If you observe offending code, please feel free to send a pull request or ping us on [Slack](https://slack.min.io).

View File

@@ -1,23 +1,34 @@
PWD := $(shell pwd)
GOPATH := $(shell go env GOPATH)
default: mcs
.PHONY: mcs
mcs:
@echo "Building mcs binary to './mcs'"
@(CGO_ENABLED=0 go build --tags=kqueue --ldflags "-s -w" -o mcs ./cmd/mcs)
@(CGO_ENABLED=0 go build -trimpath --tags=kqueue --ldflags "-s -w" -o mcs ./cmd/mcs)
install: mcs
@echo "Installing mcs binary to '$(GOPATH)/bin/mcs'"
@mkdir -p $(GOPATH)/bin && cp -f $(PWD)/mcs $(GOPATH)/bin/mcs
@echo "Installation successful. To learn more, try \"mcs --help\"."
swagger-gen:
@echo "Generating swagger server code from yaml"
@swagger generate server -A mcs --main-package=mcs --exclude-main -P models.Principal -f ./swagger.yml -r NOTICE
build:
assets:
@(cd portal-ui; yarn install; make build-static; cd ..)
@(CGO_ENABLED=0 go build --tags kqueue --ldflags "-s -w" -o mcs ./cmd/mcs)
test:
@(go test ./restapi -v)
@(go test -race -v github.com/minio/mcs/restapi/...)
@(go test -race -v github.com/minio/mcs/pkg/auth/...)
coverage:
@(go test ./restapi -v -coverprofile=coverage.out && go tool cover -html=coverage.out && open coverage.html)
@(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"
@find . -name '*.test' | xargs rm -fv
@find . -name '*~' | xargs rm -fv
@rm -vf mcs

View File

@@ -1,6 +1,6 @@
# MCS Minio Console Service
# Minio Console Server
This is a REST portal server created using [go-swagger](https://github.com/go-swagger/go-swagger)
A graphical user interface for [MinIO](https://github.com/minio/minio)
## Setup
@@ -54,40 +54,21 @@ $ mc admin policy set myminio mcsAdmin user=mcs
To run the server:
```
export MCS_HMAC_JWT_SECRET=YOURJWTSIGNINGSECRET
#required to encrypt jwet payload
export MCS_PBKDF_PASSPHRASE=SECRET
#required to encrypt jwet payload
export MCS_PBKDF_SALT=SECRET
export MCS_ACCESS_KEY=mcs
export MCS_SECRET_KEY=YOURMCSSECRET
export MCS_MINIO_SERVER=http://localhost:9000
./mcs
./mcs server
```
You can verify that the apis work by doing the request on `localhost:9090/api/v1/...`
# Development
The API handlers are created using a YAML definition located in `swagger.YAML`.
To add new api, the YAML file needs to be updated with all the desired apis using the [Swagger Basic Structure](https://swagger.io/docs/specification/2-0/basic-structure/), this includes paths, parameters, definitions, tags, etc.
## Generate server from YAML
Once the YAML file is ready we can autogenerate the code needed for the new api by just running:
Validate it:
```
swagger validate ./swagger.yml
```
Update server code:
```
make swagger-gen
```
This will update all the necessary code.
`./restapi/configure_mcs.go` is a file that contains the handlers to be used by the application, here is the only place where we need to update our code to support the new apis. This file is not affected when running the swagger generator and it is safe to edit.
## Unit Tests
`./restapi/handlers_test.go` needs to be updated with the proper tests for the new api.
To run tests:
```
go test ./restapi
```
# Contribute to mcs Project
Please follow mcs [Contributor's Guide](https://github.com/minio/mcs/blob/master/CONTRIBUTING.md)

41
SECURITY.md Normal file
View File

@@ -0,0 +1,41 @@
# Security Policy
## Supported Versions
We always provide security updates for the [latest release](https://github.com/minio/mcs/releases/latest).
Whenever there is a security update you just need to upgrade to the latest version.
## Reporting a Vulnerability
All security bugs in [minio/mcs](https://github,com/minio/mcs) (or other minio/* repositories)
should be reported by email to security@min.io. Your email will be acknowledged within 48 hours,
and you'll receive a more detailed response to your email within 72 hours indicating the next steps
in handling your report.
Please, provide a detailed explanation of the issue. In particular, outline the type of the security
issue (DoS, authentication bypass, information disclose, ...) and the assumptions you're making (e.g. do
you need access credentials for a successful exploit).
If you have not received a reply to your email within 48 hours or you have not heard from the security team
for the past five days please contact the security team directly:
- Primary security coordinator: lenin@min.io
- Secondary coordinator: daniel@min.io, cesar@min.io
- If you receive no response: dev@min.io
### Disclosure Process
MinIO uses the following disclosure process:
1. Once the security report is received one member of the security team tries to verify and reproduce
the issue and determines the impact it has.
2. A member of the security team will respond and either confirm or reject the security report.
If the report is rejected the response explains why.
3. Code is audited to find any potential similar problems.
4. Fixes are prepared for the latest release.
5. On the date that the fixes are applied a security advisory will be published on https://blog.min.io.
Please inform us in your report email whether MinIO should mention your contribution w.r.t. fixing
the security issue. By default MinIO will **not** publish this information to protect your privacy.
This process can take some time, especially when coordination is required with maintainers of other projects.
Every effort will be made to handle the bug in as timely a manner as possible, however it's important that we
follow the process described above to ensure that disclosures are handled consistently.

View File

@@ -22,7 +22,7 @@ import (
"path/filepath"
"sort"
"github.com/minio/m3/mcs/pkg"
"github.com/minio/mcs/pkg"
"github.com/minio/minio/pkg/console"
"github.com/minio/minio/pkg/trie"
@@ -31,7 +31,7 @@ import (
"github.com/minio/cli"
)
// Help template for m3.
// Help template for mcs.
var mcsHelpTemplate = `NAME:
{{.Name}} - {{.Usage}}
@@ -53,14 +53,13 @@ VERSION:
var appCmds = []cli.Command{
serverCmd,
versionCmd,
}
func newApp(name string) *cli.App {
// Collection of m3 commands currently supported are.
commands := []cli.Command{}
// Collection of mcs commands currently supported are.
var commands []cli.Command
// Collection of m3 commands currently supported in a trie tree.
// Collection of mcs commands currently supported in a trie tree.
commandsTree := trie.NewTrie()
// registerCommand registers a cli command.
@@ -106,7 +105,7 @@ func newApp(name string) *cli.App {
app.Name = name
app.Version = pkg.Version
app.Author = "MinIO, Inc."
app.Usage = "mcs COMMAND"
app.Usage = "mcs"
app.Description = `MinIO Console Server`
app.Commands = commands
app.HideHelpCommand = true // Hide `help, h` command, we already have `minio --help`.

View File

@@ -17,15 +17,15 @@
package main
import (
"flag"
"fmt"
"log"
"os"
"github.com/go-openapi/loads"
"github.com/jessevdk/go-flags"
"github.com/minio/cli"
"github.com/minio/m3/mcs/restapi"
"github.com/minio/m3/mcs/restapi/operations"
"github.com/minio/mcs/restapi"
"github.com/minio/mcs/restapi/operations"
)
// starts the server
@@ -35,10 +35,35 @@ var serverCmd = cli.Command{
Usage: "starts mcs server",
Action: startServer,
Flags: []cli.Flag{
cli.StringFlag{
Name: "host",
Value: restapi.GetHostname(),
Usage: "HTTP server hostname",
},
cli.IntFlag{
Name: "port",
Value: 9090,
Usage: "Server port",
Value: restapi.GetPort(),
Usage: "HTTP Server port",
},
cli.StringFlag{
Name: "tls-host",
Value: restapi.GetSSLHostname(),
Usage: "HTTPS server hostname",
},
cli.IntFlag{
Name: "tls-port",
Value: restapi.GetSSLPort(),
Usage: "HTTPS server port",
},
cli.StringFlag{
Name: "tls-certificate",
Value: "",
Usage: "filename of public cert",
},
cli.StringFlag{
Name: "tls-key",
Value: "",
Usage: "filename of private key",
},
},
}
@@ -74,11 +99,32 @@ func startServer(ctx *cli.Context) error {
}
os.Exit(code)
}
// Parse flags
flag.Parse()
server.ConfigureAPI()
server.Host = ctx.String("host")
server.Port = ctx.Int("port")
restapi.Hostname = ctx.String("host")
restapi.Port = fmt.Sprintf("%v", ctx.Int("port"))
tlsCertificatePath := ctx.String("tls-certificate")
tlsCertificateKeyPath := ctx.String("tls-key")
if tlsCertificatePath != "" && tlsCertificateKeyPath != "" {
server.TLSCertificate = flags.Filename(tlsCertificatePath)
server.TLSCertificateKey = flags.Filename(tlsCertificateKeyPath)
// If TLS certificates are provided enforce the HTTPS schema, meaning mcs will redirect
// plain HTTP connections to HTTPS server
server.EnabledListeners = []string{"http", "https"}
server.TLSPort = ctx.Int("tls-port")
server.TLSHost = ctx.String("tls-host")
// Need to store tls-port, tls-host un config variables so secure.middleware can read from there
restapi.TLSPort = fmt.Sprintf("%v", ctx.Int("tls-port"))
restapi.TLSHostname = ctx.String("tls-host")
restapi.TLSRedirect = "on"
}
server.ConfigureAPI()
if err := server.Serve(); err != nil {
log.Fatalln(err)
}

80
code_of_conduct.md Normal file
View File

@@ -0,0 +1,80 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior, in compliance with the
licensing terms applying to the Project developments.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful. However, these actions shall respect the
licensing terms of the Project Developments that will always supersede such
Code of Conduct.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at dev@min.io. The project team
will review and investigate all complaints, and will respond in a way that it deems
appropriate to the circumstances. The project team is obligated to maintain
confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]
This version includes a clarification to ensure that the code of conduct is in
compliance with the free software licensing terms of the project.
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

19
go.mod
View File

@@ -1,8 +1,10 @@
module github.com/minio/m3/mcs
module github.com/minio/mcs
go 1.14
require (
github.com/coreos/go-oidc v2.2.1+incompatible
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/elazarl/go-bindata-assetfs v1.0.0
github.com/go-openapi/errors v0.19.4
github.com/go-openapi/loads v0.19.5
@@ -11,11 +13,18 @@ require (
github.com/go-openapi/strfmt v0.19.5
github.com/go-openapi/swag v0.19.8
github.com/go-openapi/validate v0.19.7
github.com/gorilla/websocket v1.4.2
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-20200403024131-4d36c1f8b856
github.com/minio/minio v0.0.0-20200327214830-6f992134a25f
github.com/minio/minio-go/v6 v6.0.51-0.20200401083717-eadbcae2a0e6
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/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/satori/go.uuid v1.2.0
github.com/stretchr/testify v1.5.1
golang.org/x/net v0.0.0-20200301022130-244492dfa37a
github.com/unrolled/secure v1.0.7
golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a
)

82
go.sum
View File

@@ -2,6 +2,7 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.39.0 h1:UgQP9na6OTfp4dsAiz/eFpFA1C6tPdH5wiRdi19tuMw=
cloud.google.com/go v0.39.0/go.mod h1:rVLT6fkc8chs9sfPtFc1SBH6em7n+ZoXaG+87tDISts=
contrib.go.opencensus.io/exporter/ocagent v0.5.0 h1:TKXjQSRS0/cCDrP7KvkgU6SmILtF/yV2TOs/02K/WZQ=
contrib.go.opencensus.io/exporter/ocagent v0.5.0/go.mod h1:ImxhfLRpxoYiSq891pBrLVhN+qmP8BTVvdH2YLs7Gl0=
git.apache.org/thrift.git v0.13.0 h1:/3bz5WZ+sqYArk7MBBBbDufMxKKOA56/6JO6psDpUDY=
git.apache.org/thrift.git v0.13.0/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
@@ -9,6 +10,7 @@ github.com/Azure/azure-pipeline-go v0.2.1 h1:OLBdZJ3yvOn2MezlWvbrBMTEUQC72zAftRZ
github.com/Azure/azure-pipeline-go v0.2.1/go.mod h1:UGSo8XybXnIGZ3epmeBw7Jdz+HiUVpqIlpz/HKHylF4=
github.com/Azure/azure-storage-blob-go v0.8.0 h1:53qhf0Oxa0nOjgbDeeYPUeyiNmafAFEY95rZLK0Tj6o=
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/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
@@ -19,6 +21,7 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/Shopify/sarama v1.24.1 h1:svn9vfN3R1Hz21WR2Gj0VW9ehaDGkiOS+VqlIcZOkMI=
github.com/Shopify/sarama v1.24.1/go.mod h1:fGP8eQ6PugKEI0iUETYYtnP6d1pH/bdDMTel1X5ajsU=
github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
@@ -31,6 +34,7 @@ github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190307165228-86c17b95fcd5 h1:nWDRPC
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=
github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
@@ -38,6 +42,7 @@ 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=
@@ -47,6 +52,7 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLM
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/census-instrumentation/opencensus-proto v0.2.0 h1:LzQXZOgg4CQfE6bFvXGM30YZL1WW/M337pXml+GrcZ4=
github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -55,12 +61,16 @@ github.com/cheggaaa/pb v1.0.28/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXH
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/codegangsta/negroni v1.0.0 h1:+aYywywx4bnKXWvoWtRfJ91vC59NbEhEY03sZjQhbVY=
github.com/codegangsta/negroni v1.0.0/go.mod h1:v0y3T5G7Y1UlFfyxFn/QLRU4a2EuNau2iZY63YTKWo0=
github.com/coredns/coredns v1.4.0 h1:RubBkYmkByUqZWWkjRHvNLnUHgkRVqAWgSMmRFvpE1A=
github.com/coredns/coredns v1.4.0/go.mod h1:zASH/MVDgR6XZTbxvOnsZfffS+31vg6Ackf/wo1+AM0=
github.com/coreos/bbolt v1.3.3 h1:n6AiVyVRKQFNb6mJlwESEvvLoDyiTzXX7ORAUlkeBdY=
github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.12+incompatible h1:pAWNwdf7QiT1zfaWyqCtNZQWCLByQyA3JrSQyuYAqnQ=
github.com/coreos/etcd v3.3.12+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk=
github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8=
@@ -95,8 +105,10 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/frankban/quicktest v1.4.1/go.mod h1:36zfPVQyHxymz4cH7wlDmVwDrJuljRB60qkgn7rorfQ=
github.com/frankban/quicktest v1.7.2 h1:2QxQoC1TS09S7fhCPsrvqYdvP1H5M1P1ih5ABm3BTYk=
github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 h1:Mn26/9ZMNWSw9C9ERFA1PUxfmGpolnw2v0bKOREu5ew=
@@ -245,6 +257,8 @@ github.com/gorilla/rpc v1.2.0 h1:WvvdC2lNeT1SP32zrIce5l0ECBfbAlmrmSBsuc57wfk=
github.com/gorilla/rpc v1.2.0/go.mod h1:V4h9r+4sF5HnzqbwIez0fKSpANP0zlYd3qR7p36jkTQ=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-middleware v1.1.0 h1:THDBEeQ9xZ8JEaCLyLQqXMMdRqNr0QAUJTIkQAUtFjg=
github.com/grpc-ecosystem/go-grpc-middleware v1.1.0/go.mod h1:f5nM7jw/oeRSadq3xCzHAvxcr8HZnzsqU6ILg/0NiiE=
@@ -262,8 +276,11 @@ github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtng
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-hclog v0.9.1/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI=
github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
@@ -284,6 +301,7 @@ github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/raft v1.1.1-0.20190703171940-f639636d18e0 h1:msEDtkZC3STZq6Pthlju+jKruuNHXCZAWhghDK47HcM=
github.com/hashicorp/raft v1.1.1-0.20190703171940-f639636d18e0/go.mod h1:vPAJM8Asw6u8LxC3eJCUZmRP/E4QmUGE1R7g7k8sG/8=
github.com/hashicorp/raft-boltdb v0.0.0-20171010151810-6e5ba93211ea/go.mod h1:pNv7Wc3ycL6F5oOWn+tPGo2gWD4a5X+yp/ntwdKLjRk=
github.com/hashicorp/vault/api v1.0.4 h1:j08Or/wryXT4AcHj1oCbMd7IijXcKzYUGw59LGu9onU=
@@ -338,8 +356,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kurin/blazer v0.5.4-0.20190613185654-cf2f27cc0be3 h1:1sl2HmNtqGnDuydLgCJwZIpDLGqZOdwOkcY8WtUl8Cw=
github.com/kurin/blazer v0.5.4-0.20190613185654-cf2f27cc0be3/go.mod h1:4FCXMUWo9DllR2Do4TtBd377ezyAJ51vB5uTBjt0pGU=
github.com/kurin/blazer v0.5.4-0.20200327014341-8f90a40f8af7 h1:smZXPopqRVVywwzou4WYWvUbJvSAzIDFizfWElpmAqY=
github.com/kurin/blazer v0.5.4-0.20200327014341-8f90a40f8af7/go.mod h1:4FCXMUWo9DllR2Do4TtBd377ezyAJ51vB5uTBjt0pGU=
github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mailru/easyjson v0.0.0-20180730094502-03f2033d19d5/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
@@ -383,17 +401,18 @@ 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-20200403024131-4d36c1f8b856 h1:4uIc5fw4tVr5glh2Mc8GFuiY04pTGEhmihPxJPUvCoU=
github.com/minio/mc v0.0.0-20200403024131-4d36c1f8b856/go.mod h1:IDy4dA4aFY6zFFNkYgdUztl0jcYuev/Ubg3NadoaMKc=
github.com/minio/minio v0.0.0-20200327214830-6f992134a25f h1:RoOBi0vhXkZqe2b6RTROOsVJUwMqLMoet9r7eL01euo=
github.com/minio/minio v0.0.0-20200327214830-6f992134a25f/go.mod h1:BzbIyKUJPp+4f03i2XF7+GsijXnxMakUe5x+lm2WNc8=
github.com/minio/minio-go/v6 v6.0.45/go.mod h1:qD0lajrGW49lKZLtXKtCB4X/qkMf0a5tBvN2PaZg7Gg=
github.com/minio/minio-go/v6 v6.0.51-0.20200319192131-097caa7760c7 h1:WQmYVUDRGdcEWhJeb42/Fn1IO7SBLem173DTE4+jp/E=
github.com/minio/minio-go/v6 v6.0.51-0.20200319192131-097caa7760c7/go.mod h1:qD0lajrGW49lKZLtXKtCB4X/qkMf0a5tBvN2PaZg7Gg=
github.com/minio/minio-go/v6 v6.0.51-0.20200401083717-eadbcae2a0e6 h1:7JhqKjmt1Tv6co7eP/40/xtDLrzzWu1UwUNwQzSvcb0=
github.com/minio/minio-go/v6 v6.0.51-0.20200401083717-eadbcae2a0e6/go.mod h1:qD0lajrGW49lKZLtXKtCB4X/qkMf0a5tBvN2PaZg7Gg=
github.com/minio/parquet-go v0.0.0-20200125064549-a1e49702e174 h1:WYFHZIJ5LTWd4C3CW26jguaBLLDdX7l1/Xa3QSKGkIc=
github.com/minio/parquet-go v0.0.0-20200125064549-a1e49702e174/go.mod h1:PXYM9yI2l0YPmxHUXe6mFTmkQcyaVasDshAPTbGpDoo=
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/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/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=
github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM=
github.com/minio/simdjson-go v0.1.5-0.20200303142138-b17fe061ea37 h1:pDeao6M5AEd8hwTtGmE0pVKomlL56JFRa5SiXDZAuJE=
@@ -422,14 +441,20 @@ github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJ
github.com/montanaflynn/stats v0.5.0 h1:2EkzeTSqBB4V4bJwWrt5gIIrZmpJBcoIRGS2kWLgzmk=
github.com/montanaflynn/stats v0.5.0/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nats-io/gnatsd v1.4.1 h1:RconcfDeWpKCD6QIIwiVFcvForlXpWeJP7i5/lDLy44=
github.com/nats-io/gnatsd v1.4.1/go.mod h1:nqco77VO78hLCJpIcVfygDP2rPGfsEHkGTUk94uh5DQ=
github.com/nats-io/go-nats v1.7.2 h1:cJujlwCYR8iMz5ofZSD/p2WLW8FabhkQ2lIEVbSvNSA=
github.com/nats-io/go-nats v1.7.2/go.mod h1:+t7RHT5ApZebkrQdnn6AhQJmhJJiKAvJUio1PiiCtj0=
github.com/nats-io/go-nats-streaming v0.4.4 h1:1I3lkZDRdQYXb+holjdqZ2J6xyekrD06o9Fd8rWlgP4=
github.com/nats-io/go-nats-streaming v0.4.4/go.mod h1:gfq4R3c9sKAINOpelo0gn/b9QDMBZnmrttcsNF+lqyo=
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
github.com/nats-io/jwt v0.3.2 h1:+RB5hMpXUUA2dfxuhBTEkMOrYmM+gKIZYS1KjSostMI=
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
github.com/nats-io/nats-server v1.4.1 h1:Ul1oSOGNV/L8kjr4v6l2f9Yet6WY+LevH1/7cRZ/qyA=
github.com/nats-io/nats-server v1.4.1/go.mod h1:c8f/fHd2B6Hgms3LtCaI7y6pC4WD1f4SUxcCud5vhBc=
github.com/nats-io/nats-server/v2 v2.1.2 h1:i2Ly0B+1+rzNZHHWtD4ZwKi+OU5l+uQo1iDHZ2PmiIc=
github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
github.com/nats-io/nats-streaming-server v0.14.2 h1:WjQMDqVOwsI0Nb0E+XmEs1LY17CwHRbTCSTWKhw9fXs=
github.com/nats-io/nats-streaming-server v0.14.2/go.mod h1:RyqtDJZvMZO66YmyjIYdIvS69zu/wDAkyNWa8PIUa5c=
github.com/nats-io/nats.go v1.9.1 h1:ik3HbLhZ0YABLto7iX80pZLPw/6dx3T+++MZJwLnMrQ=
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
@@ -446,6 +471,7 @@ github.com/nsqio/go-nsq v1.0.7 h1:O0pIZJYTf+x7cZBA0UMY8WxFG79lYTURmWzAAh48ljY=
github.com/nsqio/go-nsq v1.0.7/go.mod h1:XP5zaUs3pqf+Q71EqUJs3HYfBIqfK6G83WQMdNN+Ito=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pborman/getopt v0.0.0-20180729010549-6fdd0a2c7117/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
@@ -468,6 +494,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.2-0.20190702141536-6ffe496ea953 h1:oBvgW8IvwF278gJ3R4hH0gD3ZeJxjwBXVIScRR0dRc8=
github.com/posener/complete v1.2.2-0.20190702141536-6ffe496ea953/go.mod h1:6gapUrK/U1TAN7ciCoNRIdVC5sbdBTUh1DKN0g6uH7E=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829 h1:D+CiwcpGTW6pL6bv6KI3KbyEyCKyS+1JWS2h8PNDnGA=
@@ -497,6 +525,7 @@ github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/secure-io/sio-go v0.3.0 h1:QKGb6rGJeiExac9wSWxnWPYo8O8OFN7lxXQvHshX6vo=
github.com/secure-io/sio-go v0.3.0/go.mod h1:D3KmXgKETffyYxBdFRN+Hpd2WzhzqS0EQwT3XWsAcBU=
@@ -508,8 +537,8 @@ github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/skyrings/skyring-common v0.0.0-20160929130248-d1c0bb1cbd5e h1:jrZSSgPUDtBeJbGXqgGUeupQH8I+ZvGXfhpIahye2Bc=
github.com/skyrings/skyring-common v0.0.0-20160929130248-d1c0bb1cbd5e/go.mod h1:d8hQseuYt4rJoOo21lFzYJdhMjmDqLY++ayArbgYjWI=
github.com/sirupsen/logrus v1.5.0 h1:1N5EYkVAPEywqZRJd7cwnRtCb6xJx7NH3T3WUTF980Q=
github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v0.0.0-20190401211740-f487f9de1cd3 h1:hBSHahWMEgzwRyS6dRpxY0XyjZsHyQ61s084wo5PJe0=
github.com/smartystreets/assertions v0.0.0-20190401211740-f487f9de1cd3/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
@@ -518,6 +547,8 @@ github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a h1:pa8hGb/2
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4 h1:0HKaf1o97UwFjHH9o5XsHUOF+tqmdA7KEzXLpiyaw0E=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94 h1:0ngsPmuP6XIjiFRNFYlvKwSr5zff2v+uPHaffZ6/M4k=
@@ -549,9 +580,15 @@ github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVM
github.com/ugorji/go/codec v1.1.5-pre/go.mod h1:tULtS6Gy1AE1yCENaw4Vb//HLH5njI2tfCQDUqRd8fI=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/unrolled/secure v1.0.7 h1:BcQHp3iKZyZCKj5gRqwQG+5urnGBF00wGgoPPwtheVQ=
github.com/unrolled/secure v1.0.7/go.mod h1:uGc1OcRF8gCVBA+ANksKmvM85Hka6SZtQIbrKc3sHS4=
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a h1:0R4NLDRDZX6JcmhJgXi5E4b8Wg84ihbmUKp/GvSPEzc=
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
github.com/willf/bitset v1.1.10 h1:NotGKqX0KwQ72NUzqrjZq5ipPNDQex9lo3WpaS8L2sc=
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/willf/bloom v2.0.3+incompatible h1:QDacWdqcAUI1MPOwIQZRy9kOR7yxfyEmxX8Wdm2/JPA=
github.com/willf/bloom v2.0.3+incompatible/go.mod h1:MmAltL9pDMNTrvUkxdg0k0q5I0suxmuwp3KbyrZLOZ8=
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c h1:u40Z8hqBAAQyv+vATcGgV0YCnDjqSL7/q/JyPhhJSPk=
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
@@ -614,11 +651,9 @@ golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a h1:tImsplftrFpALCYumobsd0K86vlAs/eXGFms2txfJfA=
@@ -628,6 +663,7 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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/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=
@@ -649,8 +685,8 @@ golang.org/x/sys v0.0.0-20190523142557-0e01d883c5c5/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200320181252-af34d8274f85 h1:fD99hd4ciR6T3oPhr2EkmuKe9oHixHx9Hj/hND89j3g=
golang.org/x/sys v0.0.0-20200320181252-af34d8274f85/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@@ -711,8 +747,8 @@ gopkg.in/h2non/filetype.v1 v1.0.5/go.mod h1:M0yem4rwSX5lLVrkEuRRp2/NinFMD5vgJ4Dl
gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.48.0 h1:URjZc+8ugRY5mL5uUeQH/a63JcHwdX9xZaWvmNWD7z8=
gopkg.in/ini.v1 v1.48.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.52.0 h1:j+Lt/M1oPPejkniCg1TkWE2J3Eh1oZTsHSXzMTzUXn4=
gopkg.in/ini.v1 v1.52.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.55.0 h1:E8yzL5unfpW3M6fz/eB7Cb5MQAYSZ7GKo4Qth+N2sgQ=
gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/jcmturner/aescts.v1 v1.0.1 h1:cVVZBK2b1zY26haWB4vbBiZrfFQnfbTVrE3xZq6hrEw=
gopkg.in/jcmturner/aescts.v1 v1.0.1/go.mod h1:nsR8qBOg+OucoIW+WMhB3GspUQXq9XorLnQb9XtvcOo=
gopkg.in/jcmturner/dnsutils.v1 v1.0.1 h1:cIuC1OLRGZrld+16ZJvvZxVJeKPsvd5eUIvxfoN5hSM=

View File

@@ -0,0 +1,147 @@
// 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 (
"encoding/json"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
"github.com/go-openapi/validate"
)
// AddNotificationEndpoint add notification endpoint
//
// swagger:model addNotificationEndpoint
type AddNotificationEndpoint struct {
// account
Account string `json:"account,omitempty"`
// properties
Properties map[string]string `json:"properties,omitempty"`
// service
// Enum: [webhook amqp kafka mqtt nats nsq mysql postgres elasticsearch redis]
Service string `json:"service,omitempty"`
}
// Validate validates this add notification endpoint
func (m *AddNotificationEndpoint) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateService(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
var addNotificationEndpointTypeServicePropEnum []interface{}
func init() {
var res []string
if err := json.Unmarshal([]byte(`["webhook","amqp","kafka","mqtt","nats","nsq","mysql","postgres","elasticsearch","redis"]`), &res); err != nil {
panic(err)
}
for _, v := range res {
addNotificationEndpointTypeServicePropEnum = append(addNotificationEndpointTypeServicePropEnum, v)
}
}
const (
// AddNotificationEndpointServiceWebhook captures enum value "webhook"
AddNotificationEndpointServiceWebhook string = "webhook"
// AddNotificationEndpointServiceAmqp captures enum value "amqp"
AddNotificationEndpointServiceAmqp string = "amqp"
// AddNotificationEndpointServiceKafka captures enum value "kafka"
AddNotificationEndpointServiceKafka string = "kafka"
// AddNotificationEndpointServiceMqtt captures enum value "mqtt"
AddNotificationEndpointServiceMqtt string = "mqtt"
// AddNotificationEndpointServiceNats captures enum value "nats"
AddNotificationEndpointServiceNats string = "nats"
// AddNotificationEndpointServiceNsq captures enum value "nsq"
AddNotificationEndpointServiceNsq string = "nsq"
// AddNotificationEndpointServiceMysql captures enum value "mysql"
AddNotificationEndpointServiceMysql string = "mysql"
// AddNotificationEndpointServicePostgres captures enum value "postgres"
AddNotificationEndpointServicePostgres string = "postgres"
// AddNotificationEndpointServiceElasticsearch captures enum value "elasticsearch"
AddNotificationEndpointServiceElasticsearch string = "elasticsearch"
// AddNotificationEndpointServiceRedis captures enum value "redis"
AddNotificationEndpointServiceRedis string = "redis"
)
// prop value enum
func (m *AddNotificationEndpoint) validateServiceEnum(path, location string, value string) error {
if err := validate.Enum(path, location, value, addNotificationEndpointTypeServicePropEnum); err != nil {
return err
}
return nil
}
func (m *AddNotificationEndpoint) validateService(formats strfmt.Registry) error {
if swag.IsZero(m.Service) { // not required
return nil
}
// value enum
if err := m.validateServiceEnum("service", "body", m.Service); err != nil {
return err
}
return nil
}
// MarshalBinary interface implementation
func (m *AddNotificationEndpoint) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *AddNotificationEndpoint) UnmarshalBinary(b []byte) error {
var res AddNotificationEndpoint
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -34,12 +34,13 @@ import (
// swagger:model addPolicyRequest
type AddPolicyRequest struct {
// definition
Definition string `json:"definition,omitempty"`
// name
// Required: true
Name *string `json:"name"`
// policy
// Required: true
Policy *string `json:"policy"`
}
// Validate validates this add policy request
@@ -50,6 +51,10 @@ func (m *AddPolicyRequest) Validate(formats strfmt.Registry) error {
res = append(res, err)
}
if err := m.validatePolicy(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
@@ -65,6 +70,15 @@ func (m *AddPolicyRequest) validateName(formats strfmt.Registry) error {
return nil
}
func (m *AddPolicyRequest) validatePolicy(formats strfmt.Registry) error {
if err := validate.Required("policy", "body", m.Policy); err != nil {
return err
}
return nil
}
// MarshalBinary interface implementation
func (m *AddPolicyRequest) MarshalBinary() ([]byte, error) {
if m == nil {

View File

@@ -38,6 +38,10 @@ type AddUserRequest struct {
// Required: true
AccessKey *string `json:"accessKey"`
// groups
// Required: true
Groups []string `json:"groups"`
// secret key
// Required: true
SecretKey *string `json:"secretKey"`
@@ -51,6 +55,10 @@ func (m *AddUserRequest) Validate(formats strfmt.Registry) error {
res = append(res, err)
}
if err := m.validateGroups(formats); err != nil {
res = append(res, err)
}
if err := m.validateSecretKey(formats); err != nil {
res = append(res, err)
}
@@ -70,6 +78,15 @@ func (m *AddUserRequest) validateAccessKey(formats strfmt.Registry) error {
return nil
}
func (m *AddUserRequest) validateGroups(formats strfmt.Registry) error {
if err := validate.Required("groups", "body", m.Groups); err != nil {
return err
}
return nil
}
func (m *AddUserRequest) validateSecretKey(formats strfmt.Registry) error {
if err := validate.Required("secretKey", "body", m.SecretKey); err != nil {

View File

@@ -0,0 +1,98 @@
// 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/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
"github.com/go-openapi/validate"
)
// BulkUserGroups bulk user groups
//
// swagger:model bulkUserGroups
type BulkUserGroups struct {
// groups
// Required: true
Groups []string `json:"groups"`
// users
// Required: true
Users []string `json:"users"`
}
// Validate validates this bulk user groups
func (m *BulkUserGroups) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateGroups(formats); err != nil {
res = append(res, err)
}
if err := m.validateUsers(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *BulkUserGroups) validateGroups(formats strfmt.Registry) error {
if err := validate.Required("groups", "body", m.Groups); err != nil {
return err
}
return nil
}
func (m *BulkUserGroups) validateUsers(formats strfmt.Registry) error {
if err := validate.Required("users", "body", m.Users); err != nil {
return err
}
return nil
}
// MarshalBinary interface implementation
func (m *BulkUserGroups) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *BulkUserGroups) UnmarshalBinary(b []byte) error {
var res BulkUserGroups
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -0,0 +1,98 @@
// 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/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
"github.com/go-openapi/validate"
)
// LoginOauth2AuthRequest login oauth2 auth request
//
// swagger:model loginOauth2AuthRequest
type LoginOauth2AuthRequest struct {
// code
// Required: true
Code *string `json:"code"`
// state
// Required: true
State *string `json:"state"`
}
// Validate validates this login oauth2 auth request
func (m *LoginOauth2AuthRequest) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateCode(formats); err != nil {
res = append(res, err)
}
if err := m.validateState(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *LoginOauth2AuthRequest) validateCode(formats strfmt.Registry) error {
if err := validate.Required("code", "body", m.Code); err != nil {
return err
}
return nil
}
func (m *LoginOauth2AuthRequest) validateState(formats strfmt.Registry) error {
if err := validate.Required("state", "body", m.State); err != nil {
return err
}
return nil
}
// MarshalBinary interface implementation
func (m *LoginOauth2AuthRequest) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *LoginOauth2AuthRequest) UnmarshalBinary(b []byte) error {
var res LoginOauth2AuthRequest
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -0,0 +1,104 @@
// 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 (
"encoding/json"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/validate"
)
// NofiticationService nofitication service
//
// swagger:model nofiticationService
type NofiticationService string
const (
// NofiticationServiceWebhook captures enum value "webhook"
NofiticationServiceWebhook NofiticationService = "webhook"
// NofiticationServiceAmqp captures enum value "amqp"
NofiticationServiceAmqp NofiticationService = "amqp"
// NofiticationServiceKafka captures enum value "kafka"
NofiticationServiceKafka NofiticationService = "kafka"
// NofiticationServiceMqtt captures enum value "mqtt"
NofiticationServiceMqtt NofiticationService = "mqtt"
// NofiticationServiceNats captures enum value "nats"
NofiticationServiceNats NofiticationService = "nats"
// NofiticationServiceNsq captures enum value "nsq"
NofiticationServiceNsq NofiticationService = "nsq"
// NofiticationServiceMysql captures enum value "mysql"
NofiticationServiceMysql NofiticationService = "mysql"
// NofiticationServicePostgres captures enum value "postgres"
NofiticationServicePostgres NofiticationService = "postgres"
// NofiticationServiceElasticsearch captures enum value "elasticsearch"
NofiticationServiceElasticsearch NofiticationService = "elasticsearch"
// NofiticationServiceRedis captures enum value "redis"
NofiticationServiceRedis NofiticationService = "redis"
)
// for schema
var nofiticationServiceEnum []interface{}
func init() {
var res []NofiticationService
if err := json.Unmarshal([]byte(`["webhook","amqp","kafka","mqtt","nats","nsq","mysql","postgres","elasticsearch","redis"]`), &res); err != nil {
panic(err)
}
for _, v := range res {
nofiticationServiceEnum = append(nofiticationServiceEnum, v)
}
}
func (m NofiticationService) validateNofiticationServiceEnum(path, location string, value NofiticationService) error {
if err := validate.Enum(path, location, value, nofiticationServiceEnum); err != nil {
return err
}
return nil
}
// Validate validates this nofitication service
func (m NofiticationService) Validate(formats strfmt.Registry) error {
var res []error
// value enum
if err := m.validateNofiticationServiceEnum("", "body", m); err != nil {
return err
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}

View File

@@ -0,0 +1,97 @@
// 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 (
"strconv"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// NotifEndpointResponse notif endpoint response
//
// swagger:model notifEndpointResponse
type NotifEndpointResponse struct {
// notification endpoints
NotificationEndpoints []*NotificationEndpointItem `json:"notification_endpoints"`
}
// Validate validates this notif endpoint response
func (m *NotifEndpointResponse) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateNotificationEndpoints(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *NotifEndpointResponse) validateNotificationEndpoints(formats strfmt.Registry) error {
if swag.IsZero(m.NotificationEndpoints) { // not required
return nil
}
for i := 0; i < len(m.NotificationEndpoints); i++ {
if swag.IsZero(m.NotificationEndpoints[i]) { // not required
continue
}
if m.NotificationEndpoints[i] != nil {
if err := m.NotificationEndpoints[i].Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("notification_endpoints" + "." + strconv.Itoa(i))
}
return err
}
}
}
return nil
}
// MarshalBinary interface implementation
func (m *NotifEndpointResponse) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *NotifEndpointResponse) UnmarshalBinary(b []byte) error {
var res NotifEndpointResponse
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -0,0 +1,129 @@
// 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 (
"strconv"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
"github.com/go-openapi/validate"
)
// NotificationDeleteRequest notification delete request
//
// swagger:model notificationDeleteRequest
type NotificationDeleteRequest struct {
// filter specific type of event. Defaults to all event (default: '[put,delete,get]')
// Required: true
// Min Length: 1
Events []NotificationEventType `json:"events"`
// filter event associated to the specified prefix
// Required: true
Prefix *string `json:"prefix"`
// filter event associated to the specified suffix
// Required: true
Suffix *string `json:"suffix"`
}
// Validate validates this notification delete request
func (m *NotificationDeleteRequest) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateEvents(formats); err != nil {
res = append(res, err)
}
if err := m.validatePrefix(formats); err != nil {
res = append(res, err)
}
if err := m.validateSuffix(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *NotificationDeleteRequest) validateEvents(formats strfmt.Registry) error {
if err := validate.Required("events", "body", m.Events); err != nil {
return err
}
for i := 0; i < len(m.Events); i++ {
if err := m.Events[i].Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("events" + "." + strconv.Itoa(i))
}
return err
}
}
return nil
}
func (m *NotificationDeleteRequest) validatePrefix(formats strfmt.Registry) error {
if err := validate.Required("prefix", "body", m.Prefix); err != nil {
return err
}
return nil
}
func (m *NotificationDeleteRequest) validateSuffix(formats strfmt.Registry) error {
if err := validate.Required("suffix", "body", m.Suffix); err != nil {
return err
}
return nil
}
// MarshalBinary interface implementation
func (m *NotificationDeleteRequest) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *NotificationDeleteRequest) UnmarshalBinary(b []byte) error {
var res NotificationDeleteRequest
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -0,0 +1,114 @@
// 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/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
"github.com/go-openapi/validate"
)
// NotificationEndpoint notification endpoint
//
// swagger:model notificationEndpoint
type NotificationEndpoint struct {
// account id
// Required: true
AccountID *string `json:"account_id"`
// properties
// Required: true
Properties map[string]string `json:"properties"`
// service
// Required: true
Service NofiticationService `json:"service"`
}
// Validate validates this notification endpoint
func (m *NotificationEndpoint) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateAccountID(formats); err != nil {
res = append(res, err)
}
if err := m.validateProperties(formats); err != nil {
res = append(res, err)
}
if err := m.validateService(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *NotificationEndpoint) validateAccountID(formats strfmt.Registry) error {
if err := validate.Required("account_id", "body", m.AccountID); err != nil {
return err
}
return nil
}
func (m *NotificationEndpoint) validateProperties(formats strfmt.Registry) error {
return nil
}
func (m *NotificationEndpoint) validateService(formats strfmt.Registry) error {
if err := m.Service.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("service")
}
return err
}
return nil
}
// MarshalBinary interface implementation
func (m *NotificationEndpoint) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *NotificationEndpoint) UnmarshalBinary(b []byte) error {
var res NotificationEndpoint
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -0,0 +1,92 @@
// 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/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// NotificationEndpointItem notification endpoint item
//
// swagger:model notificationEndpointItem
type NotificationEndpointItem struct {
// account id
AccountID string `json:"account_id,omitempty"`
// service
Service NofiticationService `json:"service,omitempty"`
// status
Status string `json:"status,omitempty"`
}
// Validate validates this notification endpoint item
func (m *NotificationEndpointItem) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateService(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *NotificationEndpointItem) validateService(formats strfmt.Registry) error {
if swag.IsZero(m.Service) { // not required
return nil
}
if err := m.Service.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("service")
}
return err
}
return nil
}
// MarshalBinary interface implementation
func (m *NotificationEndpointItem) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *NotificationEndpointItem) UnmarshalBinary(b []byte) error {
var res NotificationEndpointItem
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -23,9 +23,6 @@ package models
// Editing this file might prove futile when you re-run the swagger generate command
import (
"strconv"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
@@ -38,49 +35,12 @@ type Policy struct {
// name
Name string `json:"name,omitempty"`
// statements
Statements []*Statement `json:"statements"`
// version
Version string `json:"version,omitempty"`
// policy
Policy string `json:"policy,omitempty"`
}
// Validate validates this policy
func (m *Policy) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateStatements(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *Policy) validateStatements(formats strfmt.Registry) error {
if swag.IsZero(m.Statements) { // not required
return nil
}
for i := 0; i < len(m.Statements); i++ {
if swag.IsZero(m.Statements[i]) { // not required
continue
}
if m.Statements[i] != nil {
if err := m.Statements[i].Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("statements" + "." + strconv.Itoa(i))
}
return err
}
}
}
return nil
}

View File

@@ -0,0 +1,72 @@
// 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"
"github.com/go-openapi/swag"
)
// PolicyStatement policy statement
//
// swagger:model policyStatement
type PolicyStatement struct {
// action
Action []string `json:"Action"`
// condition
Condition string `json:"Condition,omitempty"`
// effect
Effect string `json:"Effect,omitempty"`
// resource
Resource string `json:"Resource,omitempty"`
// sid
Sid string `json:"Sid,omitempty"`
}
// Validate validates this policy statement
func (m *PolicyStatement) Validate(formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *PolicyStatement) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *PolicyStatement) UnmarshalBinary(b []byte) error {
var res PolicyStatement
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -0,0 +1,62 @@
// 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 (
"strconv"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// PolicyStatements policy statements
//
// swagger:model policyStatements
type PolicyStatements []*PolicyStatement
// Validate validates this policy statements
func (m PolicyStatements) Validate(formats strfmt.Registry) error {
var res []error
for i := 0; i < len(m); i++ {
if swag.IsZero(m[i]) { // not required
continue
}
if m[i] != nil {
if err := m[i].Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName(strconv.Itoa(i))
}
return err
}
}
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}

View File

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

View File

@@ -0,0 +1,63 @@
// 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"
"github.com/go-openapi/swag"
)
// ServiceAccountCreds service account creds
//
// swagger:model serviceAccountCreds
type ServiceAccountCreds struct {
// access key
AccessKey string `json:"accessKey,omitempty"`
// secret key
SecretKey string `json:"secretKey,omitempty"`
}
// Validate validates this service account creds
func (m *ServiceAccountCreds) Validate(formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *ServiceAccountCreds) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *ServiceAccountCreds) UnmarshalBinary(b []byte) error {
var res ServiceAccountCreds
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

98
models/update_user.go Normal file
View File

@@ -0,0 +1,98 @@
// 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/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
"github.com/go-openapi/validate"
)
// UpdateUser update user
//
// swagger:model updateUser
type UpdateUser struct {
// groups
// Required: true
Groups []string `json:"groups"`
// status
// Required: true
Status *string `json:"status"`
}
// Validate validates this update user
func (m *UpdateUser) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateGroups(formats); err != nil {
res = append(res, err)
}
if err := m.validateStatus(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *UpdateUser) validateGroups(formats strfmt.Registry) error {
if err := validate.Required("groups", "body", m.Groups); err != nil {
return err
}
return nil
}
func (m *UpdateUser) validateStatus(formats strfmt.Registry) error {
if err := validate.Required("status", "body", m.Status); err != nil {
return err
}
return nil
}
// MarshalBinary interface implementation
func (m *UpdateUser) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *UpdateUser) UnmarshalBinary(b []byte) error {
var res UpdateUser
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -0,0 +1,81 @@
// 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/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
"github.com/go-openapi/validate"
)
// UpdateUserGroups update user groups
//
// swagger:model updateUserGroups
type UpdateUserGroups struct {
// groups
// Required: true
Groups []string `json:"groups"`
}
// Validate validates this update user groups
func (m *UpdateUserGroups) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateGroups(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *UpdateUserGroups) validateGroups(formats strfmt.Registry) error {
if err := validate.Required("groups", "body", m.Groups); err != nil {
return err
}
return nil
}
// MarshalBinary interface implementation
func (m *UpdateUserGroups) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *UpdateUserGroups) UnmarshalBinary(b []byte) error {
var res UpdateUserGroups
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

49
pkg/auth/idp.go Normal file
View File

@@ -0,0 +1,49 @@
// 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 (
"context"
"github.com/minio/mcs/pkg/auth/idp/oauth2"
)
// IdentityProviderClient interface with all functions to be implemented
// by mock when testing, it should include all IdentityProviderClient respective api calls
// that are used within this project.
type IdentityProviderClient interface {
VerifyIdentity(ctx context.Context, code, state string) (*oauth2.User, error)
GenerateLoginURL() string
}
// Interface implementation
//
// Define the structure of a IdentityProvider Client and define the functions that are actually used
// during the authentication flow.
type IdentityProvider struct {
Client IdentityProviderClient
}
// VerifyIdentity will verify the user identity against the idp using the authorization code flow
func (c IdentityProvider) VerifyIdentity(ctx context.Context, code, state string) (*oauth2.User, error) {
return c.Client.VerifyIdentity(ctx, code, state)
}
// GenerateLoginURL returns a new URL used by the user to login against the idp
func (c IdentityProvider) GenerateLoginURL() string {
return c.Client.GenerateLoginURL()
}

View File

@@ -0,0 +1,71 @@
// 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 oauth2 contains all the necessary configurations to initialize the
// idp communication using oauth2 protocol
package oauth2
import (
"github.com/minio/mcs/pkg/auth/utils"
"github.com/minio/minio/pkg/env"
)
func GetIdpURL() string {
return env.Get(McsIdpURL, "")
}
func GetIdpClientID() string {
return env.Get(McsIdpClientID, "")
}
func GetIdpSecret() string {
return env.Get(McsIdpSecret, "")
}
// Public endpoint used by the identity oidcProvider when redirecting the user after identity verification
func GetIdpCallbackURL() string {
return env.Get(McsIdpCallbackURL, "")
}
func GetIdpAdminRoles() string {
return env.Get(McsIdpAdminRoles, "")
}
func IsIdpEnabled() bool {
return GetIdpURL() != "" &&
GetIdpClientID() != "" &&
GetIdpSecret() != "" &&
GetIdpCallbackURL() != ""
}
var defaultPassphraseForIdpHmac = utils.RandomCharString(64)
// GetPassphraseForIdpHmac returns passphrase for the pbkdf2 function used to sign the oauth2 state parameter
func getPassphraseForIdpHmac() string {
return env.Get(McsIdpHmacPassphrase, defaultPassphraseForIdpHmac)
}
var defaultSaltForIdpHmac = utils.RandomCharString(64)
// GetSaltForIdpHmac returns salt for the pbkdf2 function used to sign the oauth2 state parameter
func getSaltForIdpHmac() string {
return env.Get(McsIdpHmacSalt, defaultSaltForIdpHmac)
}
// GetSaltForIdpHmac returns the policy to be assigned to the users authenticating via an IDP
func GetIDPPolicyForUser() string {
return env.Get(McsIdpPolicyUser, "mcsAdmin")
}

View File

@@ -0,0 +1,29 @@
// 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 oauth2
const (
// const for idp configuration
McsIdpURL = "MCS_IDP_URL"
McsIdpClientID = "MCS_IDP_CLIENT_ID"
McsIdpSecret = "MCS_IDP_SECRET"
McsIdpCallbackURL = "MCS_IDP_CALLBACK"
McsIdpAdminRoles = "MCS_IDP_ADMIN_ROLES"
McsIdpHmacPassphrase = "MCS_IDP_HMAC_PASSPHRASE"
McsIdpHmacSalt = "MCS_IDP_HMAC_SALT"
McsIdpPolicyUser = "MCS_IDP_POLICY_USER"
)

View File

@@ -0,0 +1,229 @@
// 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 oauth2
import (
"context"
"crypto/sha1"
"encoding/base64"
"errors"
"fmt"
"log"
"net/http"
"net/url"
"strings"
"github.com/coreos/go-oidc"
"github.com/minio/mcs/pkg/auth/utils"
"golang.org/x/crypto/pbkdf2"
xoauth2 "golang.org/x/oauth2"
)
var (
errGeneric = errors.New("an error occurred, please try again")
)
type Configuration interface {
Exchange(ctx context.Context, code string, opts ...xoauth2.AuthCodeOption) (*xoauth2.Token, error)
AuthCodeURL(state string, opts ...xoauth2.AuthCodeOption) string
PasswordCredentialsToken(ctx context.Context, username string, password string) (*xoauth2.Token, error)
Client(ctx context.Context, t *xoauth2.Token) *http.Client
TokenSource(ctx context.Context, t *xoauth2.Token) xoauth2.TokenSource
}
type Config struct {
xoauth2.Config
}
func (ac Config) Exchange(ctx context.Context, code string, opts ...xoauth2.AuthCodeOption) (*xoauth2.Token, error) {
return ac.Exchange(ctx, code, opts...)
}
func (ac Config) AuthCodeURL(state string, opts ...xoauth2.AuthCodeOption) string {
return ac.AuthCodeURL(state, opts...)
}
func (ac Config) PasswordCredentialsToken(ctx context.Context, username string, password string) (*xoauth2.Token, error) {
return ac.PasswordCredentialsToken(ctx, username, password)
}
func (ac Config) Client(ctx context.Context, t *xoauth2.Token) *http.Client {
return ac.Client(ctx, t)
}
func (ac Config) TokenSource(ctx context.Context, t *xoauth2.Token) xoauth2.TokenSource {
return ac.TokenSource(ctx, t)
}
// Provider is a wrapper of the oauth2 configuration and the oidc provider
type Provider struct {
// oauth2Config is an interface configuration that contains the following fields
// Config{
// ClientID string
// ClientSecret string
// RedirectURL string
// Endpoint oauth2.Endpoint
// Scopes []string
// }
// - ClientID is the public identifier for this application
// - ClientSecret is a shared secret between this application and the authorization server
// - RedirectURL is the URL to redirect users going through
// the OAuth flow, after the resource owner's URLs.
// - Endpoint contains the resource server's token endpoint
// URLs. These are constants specific to each server and are
// often available via site-specific packages, such as
// google.Endpoint or github.Endpoint.
// - Scopes specifies optional requested permissions.
ClientID string
oauth2Config Configuration
oidcProvider *oidc.Provider
}
// derivedKey is the key used to compute the HMAC for signing the oauth state parameter
// its derived using pbkdf on MCS_IDP_HMAC_PASSPHRASE with MCS_IDP_HMAC_SALT
var derivedKey = pbkdf2.Key([]byte(getPassphraseForIdpHmac()), []byte(getSaltForIdpHmac()), 4096, 32, sha1.New)
// NewOauth2ProviderClient instantiates a new oauth2 client using the configured credentials
// it returns a *Provider object that contains the necessary configuration to initiate an
// oauth2 authentication flow
func NewOauth2ProviderClient(ctx context.Context, scopes []string) (*Provider, error) {
provider, err := oidc.NewProvider(ctx, GetIdpURL())
if err != nil {
return nil, err
}
// If provided scopes are empty we use a default list
if len(scopes) == 0 {
scopes = []string{oidc.ScopeOpenID, "profile", "app_metadata", "user_metadata", "email"}
}
client := new(Provider)
config := xoauth2.Config{
ClientID: GetIdpClientID(),
ClientSecret: GetIdpSecret(),
RedirectURL: GetIdpCallbackURL(),
Endpoint: provider.Endpoint(),
Scopes: scopes,
}
client.oauth2Config = &config
client.oidcProvider = provider
client.ClientID = GetIdpClientID()
return client, nil
}
type User struct {
AppMetadata map[string]interface{} `json:"app_metadata"`
Blocked bool `json:"blocked"`
CreatedAt string `json:"created_at"`
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
FamilyName string `json:"family_name"`
GivenName string `json:"given_name"`
Identities []interface{} `json:"identities"`
LastIP string `json:"last_ip"`
LastLogin string `json:"last_login"`
LastPasswordReset string `json:"last_password_reset"`
LoginsCount int `json:"logins_count"`
Mltifactor string `json:"multifactor"`
Name string `json:"name"`
Nickname string `json:"nickname"`
PhoneNumber string `json:"phone_number"`
PhoneVerified bool `json:"phone_verified"`
Picture string `json:"picture"`
UpdatedAt string `json:"updated_at"`
UserID string `json:"user_id"`
UserMetadata map[string]interface{} `json:"user_metadata"`
Username string `json:"username"`
}
// VerifyIdentity will contact the configured IDP and validate the user identity based on the authorization code
func (client *Provider) VerifyIdentity(ctx context.Context, code, state string) (*User, error) {
// verify the provided state is valid (prevents CSRF attacks)
if !validateOauth2State(state) {
return nil, errGeneric
}
// verify the authorization code against the identity oidcProvider
// idp will return a token in exchange
token, err := client.oauth2Config.Exchange(ctx, code)
if err != nil {
log.Println("Failed to verify authorization code", err)
return nil, errGeneric
}
// extract and check id_token field is provided in the response
rawIDToken, ok := token.Extra("id_token").(string)
if !ok {
log.Println("No id_token field in oauth2 token")
return nil, errGeneric
}
config := &oidc.Config{
ClientID: client.ClientID,
}
idToken, err := client.oidcProvider.Verifier(config).Verify(ctx, rawIDToken)
if err != nil {
log.Println("Failed to verify ID token", err)
return nil, errGeneric
}
var profile User
// Populate the profile object using the claims included in the token
if err := idToken.Claims(&profile); err != nil {
log.Println("Failed to read profile information", err)
return nil, errGeneric
}
return &profile, nil
}
// validateOauth2State validates the provided state was originated using the same
// instance (or one configured using the same secrets) of MCS, this is basically used to prevent CSRF attacks
// https://security.stackexchange.com/questions/20187/oauth2-cross-site-request-forgery-and-state-parameter
func validateOauth2State(state string) bool {
// state contains a base64 encoded string that may ends with "==", the browser encodes that to "%3D%3D"
// query unescape is need it before trying to decode the base64 string
encodedMessage, err := url.QueryUnescape(state)
if err != nil {
log.Println(err)
return false
}
// decode the state parameter value
message, err := base64.StdEncoding.DecodeString(encodedMessage)
if err != nil {
log.Println(err)
return false
}
s := strings.Split(string(message), ":")
// Validate that the decoded message has the right format "message:hmac"
if len(s) != 2 {
return false
}
// extract the state and hmac
incomingState, incomingHmac := s[0], s[1]
// validate that hmac(incomingState + pbkdf2(secret, salt)) == incomingHmac
return utils.ComputeHmac256(incomingState, derivedKey) == incomingHmac
}
// GetRandomStateWithHMAC computes message + hmac(message, pbkdf2(key, salt)) to be used as state during the oauth authorization
func GetRandomStateWithHMAC(length int) string {
state := utils.RandomCharString(length)
hmac := utils.ComputeHmac256(state, derivedKey)
return base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", state, hmac)))
}
// GenerateLoginURL returns a new login URL based on the configured IDP
func (client *Provider) GenerateLoginURL() string {
// generates random state and sign it using HMAC256
state := GetRandomStateWithHMAC(25)
loginURL := client.oauth2Config.AuthCodeURL(state)
return strings.TrimSpace(loginURL)
}

View File

@@ -0,0 +1,98 @@
// 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 oauth2
import (
"context"
"net/http"
"testing"
"github.com/coreos/go-oidc"
"github.com/stretchr/testify/assert"
"golang.org/x/oauth2"
)
type Oauth2configMock struct{}
var oauth2ConfigExchangeMock func(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error)
var oauth2ConfigAuthCodeURLMock func(state string, opts ...oauth2.AuthCodeOption) string
var oauth2ConfigPasswordCredentialsTokenMock func(ctx context.Context, username string, password string) (*oauth2.Token, error)
var oauth2ConfigClientMock func(ctx context.Context, t *oauth2.Token) *http.Client
var oauth2ConfigokenSourceMock func(ctx context.Context, t *oauth2.Token) oauth2.TokenSource
func (ac Oauth2configMock) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
return oauth2ConfigExchangeMock(ctx, code, opts...)
}
func (ac Oauth2configMock) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string {
return oauth2ConfigAuthCodeURLMock(state, opts...)
}
func (ac Oauth2configMock) PasswordCredentialsToken(ctx context.Context, username string, password string) (*oauth2.Token, error) {
return oauth2ConfigPasswordCredentialsTokenMock(ctx, username, password)
}
func (ac Oauth2configMock) Client(ctx context.Context, t *oauth2.Token) *http.Client {
return oauth2ConfigClientMock(ctx, t)
}
func (ac Oauth2configMock) TokenSource(ctx context.Context, t *oauth2.Token) oauth2.TokenSource {
return oauth2ConfigokenSourceMock(ctx, t)
}
func TestGenerateLoginURL(t *testing.T) {
funcAssert := assert.New(t)
oauth2Provider := Provider{
oauth2Config: Oauth2configMock{},
oidcProvider: &oidc.Provider{},
}
// Test-1 : GenerateLoginURL() generates URL correctly with provided state
oauth2ConfigAuthCodeURLMock = func(state string, opts ...oauth2.AuthCodeOption) string {
// Internally we are testing the private method getRandomStateWithHMAC, this function should always returns
// a non-empty string
return state
}
url := oauth2Provider.GenerateLoginURL()
funcAssert.NotEqual("", url)
}
func TestVerifyIdentity(t *testing.T) {
ctx := context.Background()
funcAssert := assert.New(t)
// mock data
oauth2Provider := Provider{
oauth2Config: Oauth2configMock{},
oidcProvider: &oidc.Provider{},
}
// Test-1 : VerifyIdentity() should fail because of bad state token
_, err := oauth2Provider.VerifyIdentity(ctx, "AAABBBCCCDDDEEEFFF", "badtoken")
funcAssert.NotNil(err)
// Test-2 : VerifyIdentity() should fail because no id_token is provided by the idp
oauth2ConfigExchangeMock = func(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
return &oauth2.Token{}, nil
}
state := GetRandomStateWithHMAC(32)
code := "AAABBBCCCDDDEEEFFF"
_, err = oauth2Provider.VerifyIdentity(ctx, code, state)
funcAssert.NotNil(err)
// Test-3 : VerifyIdentity() should fail because no id_token is provided by the idp
// TODO
// Test-4 : VerifyIdentity() should fail because oidcProvider.Verifier returned an error
// TODO
// Test-5 : VerifyIdentity() should fail because idToken.Claims contains invalid fields
// TODO
}

181
pkg/auth/jwt.go Normal file
View File

@@ -0,0 +1,181 @@
// 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 (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha1"
"encoding/base64"
"errors"
"fmt"
"io"
"log"
"strings"
jwtgo "github.com/dgrijalva/jwt-go"
xjwt "github.com/minio/mcs/pkg/auth/jwt"
"github.com/minio/minio-go/v6/pkg/credentials"
"github.com/minio/minio/cmd"
uuid "github.com/satori/go.uuid"
"golang.org/x/crypto/pbkdf2"
)
var (
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")
)
// derivedKey is the key used to encrypt the JWT claims, its derived using pbkdf on MCS_PBKDF_PASSPHRASE with MCS_PBKDF_SALT
var derivedKey = pbkdf2.Key([]byte(xjwt.GetPBKDFPassphrase()), []byte(xjwt.GetPBKDFSalt()), 4096, 32, sha1.New)
// IsJWTValid returns true or false depending if the provided jwt is valid or not
func IsJWTValid(token string) bool {
_, err := JWTAuthenticate(token)
return err == nil
}
// DecryptedClaims claims struct for decrypted credentials
type DecryptedClaims struct {
AccessKeyID string
SecretAccessKey string
SessionToken string
}
// JWTAuthenticate takes a jwt, decode it, extract claims and validate the signature
// if the jwt claims.Data is valid we proceed to decrypt the information inside
//
// returns claims after validation in the following format:
//
// type DecryptedClaims struct {
// AccessKeyID
// SecretAccessKey
// SessionToken
// }
func JWTAuthenticate(token string) (*DecryptedClaims, error) {
if token == "" {
return nil, errNoAuthToken
}
// initialize claims object
claims := xjwt.NewMapClaims()
// populate the claims object
if err := xjwt.ParseWithClaims(token, claims); err != nil {
return nil, errAuthentication
}
// decrypt the claims.Data field
claimTokens, err := decryptClaims(claims.Data)
if err != nil {
// we print decryption token error information for debugging purposes
log.Println(err)
// we return a generic error that doesn't give any information to attackers
return nil, errReadingToken
}
// claimsTokens contains the decrypted STS claims
return claimTokens, nil
}
// 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) {
if credentials != nil {
encryptedClaims, err := encryptClaims(credentials.AccessKeyID, credentials.SecretAccessKey, credentials.SessionToken)
if err != nil {
return "", err
}
claims := xjwt.NewStandardClaims()
claims.SetExpiry(cmd.UTCNow().Add(xjwt.GetMcsSTSAndJWTDurationTime()))
claims.SetSubject(uuid.NewV4().String())
claims.SetData(encryptedClaims)
claims.SetAudience(audience)
jwt := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, claims)
return jwt.SignedString([]byte(xjwt.GetHmacJWTSecret()))
}
return "", errors.New("provided credentials are empty")
}
// 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))
ciphertext, err := encrypt(payload)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
// decryptClaims() receives base64 encoded ciphertext, decode it, decrypt it (AES-GCM) and produces a *DecryptedClaims object
func decryptClaims(ciphertext string) (*DecryptedClaims, error) {
decoded, err := base64.StdEncoding.DecodeString(ciphertext)
if err != nil {
log.Println(err)
return nil, errClaimsFormat
}
plaintext, err := decrypt(decoded)
if err != nil {
log.Println(err)
return nil, errClaimsFormat
}
s := strings.Split(string(plaintext), ":")
// Validate that the decrypted string has the right format "accessKeyID:secretAccessKey:sessionToken"
if len(s) != 3 {
return nil, errClaimsFormat
}
accessKeyID, secretAccessKey, sessionToken := s[0], s[1], s[2]
return &DecryptedClaims{
AccessKeyID: accessKeyID,
SecretAccessKey: secretAccessKey,
SessionToken: sessionToken,
}, nil
}
// Encrypt a blob of data using AEAD (AES-GCM) with a pbkdf2 derived key
func encrypt(plaintext []byte) ([]byte, error) {
block, _ := aes.NewCipher(derivedKey)
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
cipherText := gcm.Seal(nonce, nonce, plaintext, nil)
return cipherText, nil
}
// Decrypts a blob of data using AEAD (AES-GCM) with a pbkdf2 derived key
func decrypt(data []byte) ([]byte, error) {
block, err := aes.NewCipher(derivedKey)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonceSize := gcm.NonceSize()
nonce, cipherText := data[:nonceSize], data[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, cipherText, nil)
if err != nil {
return nil, err
}
return plaintext, nil
}

63
pkg/auth/jwt/config.go Normal file
View File

@@ -0,0 +1,63 @@
// 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 jwt
import (
"strconv"
"time"
"github.com/minio/mcs/pkg/auth/utils"
"github.com/minio/minio/pkg/env"
)
// defaultHmacJWTPassphrase will be used by default if application is not configured with a custom MCS_HMAC_JWT_SECRET secret
var defaultHmacJWTPassphrase = utils.RandomCharString(64)
// GetHmacJWTSecret returns the 64 bytes secret used for signing the generated JWT for the application
func GetHmacJWTSecret() string {
return env.Get(McsHmacJWTSecret, defaultHmacJWTPassphrase)
}
// McsSTSAndJWTDurationSeconds returns the default session duration for the STS requested tokens and the generated JWTs.
// Ideally both values should match so jwt and Minio sts sessions expires at the same time.
func GetMcsSTSAndJWTDurationInSeconds() int {
duration, err := strconv.Atoi(env.Get(McsSTSAndJWTDurationSeconds, "3600"))
if err != nil {
duration = 3600
}
return duration
}
// GetMcsSTSAndJWTDurationTime returns GetMcsSTSAndJWTDurationInSeconds in duration format
func GetMcsSTSAndJWTDurationTime() time.Duration {
duration := GetMcsSTSAndJWTDurationInSeconds()
return time.Duration(duration) * time.Second
}
var defaultPBKDFPassphrase = utils.RandomCharString(64)
// GetPBKDFPassphrase returns passphrase for the pbkdf2 function used to encrypt JWT payload
func GetPBKDFPassphrase() string {
return env.Get(McsPBKDFPassphrase, defaultPBKDFPassphrase)
}
var defaultPBKDFSalt = utils.RandomCharString(64)
// GetPBKDFSalt returns salt for the pbkdf2 function used to encrypt JWT payload
func GetPBKDFSalt() string {
return env.Get(McsPBKDFSalt, defaultPBKDFSalt)
}

24
pkg/auth/jwt/const.go Normal file
View File

@@ -0,0 +1,24 @@
// 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 jwt
const (
McsHmacJWTSecret = "MCS_HMAC_JWT_SECRET"
McsSTSAndJWTDurationSeconds = "MCS_STS_AND_JWT_DURATION_SECONDS"
McsPBKDFPassphrase = "MCS_PBKDF_PASSPHRASE"
McsPBKDFSalt = "MCS_PBKDF_SALT"
)

281
pkg/auth/jwt/parser.go Normal file
View File

@@ -0,0 +1,281 @@
// 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 jwt
// This file is a re-implementation of the original code here with some
// additional allocation tweaks reproduced using GODEBUG=allocfreetrace=1
// original file https://github.com/dgrijalva/jwt-go/blob/master/parser.go
// borrowed under MIT License https://github.com/dgrijalva/jwt-go/blob/master/LICENSE
import (
"crypto"
"crypto/hmac"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"sync"
"time"
jwtgo "github.com/dgrijalva/jwt-go"
jsoniter "github.com/json-iterator/go"
)
const (
claimData = "data"
claimSub = "sub"
)
// SigningMethodHMAC - Implements the HMAC-SHA family of signing methods signing methods
// Expects key type of []byte for both signing and validation
type SigningMethodHMAC struct {
Name string
Hash crypto.Hash
}
// Specific instances for HS256, HS384, HS512
var (
SigningMethodHS256 *SigningMethodHMAC
SigningMethodHS384 *SigningMethodHMAC
SigningMethodHS512 *SigningMethodHMAC
)
var (
base64BufPool sync.Pool
hmacSigners []*SigningMethodHMAC
)
func init() {
base64BufPool = sync.Pool{
New: func() interface{} {
buf := make([]byte, 8192)
return &buf
},
}
hmacSigners = []*SigningMethodHMAC{
{"HS256", crypto.SHA256},
{"HS384", crypto.SHA384},
{"HS512", crypto.SHA512},
}
}
// StandardClaims are basically standard claims with "Data"
type StandardClaims struct {
Data string `json:"data,omitempty"`
jwtgo.StandardClaims
}
// MapClaims - implements custom unmarshaller
type MapClaims struct {
Data string `json:"data,omitempty"`
Subject string `json:"sub,omitempty"`
jwtgo.MapClaims
}
// NewStandardClaims - initializes standard claims
func NewStandardClaims() *StandardClaims {
return &StandardClaims{}
}
// SetIssuer sets issuer for these claims
func (c *StandardClaims) SetIssuer(issuer string) {
c.Issuer = issuer
}
// SetAudience sets audience for these claims
func (c *StandardClaims) SetAudience(aud string) {
c.Audience = aud
}
// SetExpiry sets expiry in unix epoch secs
func (c *StandardClaims) SetExpiry(t time.Time) {
c.ExpiresAt = t.Unix()
}
// SetSubject sets unique identifier for the jwt
func (c *StandardClaims) SetSubject(subject string) {
c.Subject = subject
}
// SetData sets the "Data" custom field.
func (c *StandardClaims) SetData(data string) {
c.Data = data
}
// Valid - implements https://godoc.org/github.com/dgrijalva/jwt-go#Claims compatible
// claims interface, additionally validates "Data" field.
func (c *StandardClaims) Valid() error {
if err := c.StandardClaims.Valid(); err != nil {
return err
}
if c.Data == "" || c.Subject == "" {
return jwtgo.NewValidationError("data/sub",
jwtgo.ValidationErrorClaimsInvalid)
}
return nil
}
// NewMapClaims - Initializes a new map claims
func NewMapClaims() *MapClaims {
return &MapClaims{MapClaims: jwtgo.MapClaims{}}
}
// Lookup returns the value and if the key is found.
func (c *MapClaims) Lookup(key string) (value string, ok bool) {
var vinterface interface{}
vinterface, ok = c.MapClaims[key]
if ok {
value, ok = vinterface.(string)
}
return
}
// SetExpiry sets expiry in unix epoch secs
func (c *MapClaims) SetExpiry(t time.Time) {
c.MapClaims["exp"] = t.Unix()
}
// SetData sets the "Data" custom field.
func (c *MapClaims) SetData(data string) {
c.MapClaims[claimData] = data
}
// Valid - implements https://godoc.org/github.com/dgrijalva/jwt-go#Claims compatible
// claims interface, additionally validates "Data" field.
func (c *MapClaims) Valid() error {
if err := c.MapClaims.Valid(); err != nil {
return err
}
if c.Data == "" || c.Subject == "" {
return jwtgo.NewValidationError("data/subject",
jwtgo.ValidationErrorClaimsInvalid)
}
return nil
}
// Map returns underlying low-level map claims.
func (c *MapClaims) Map() map[string]interface{} {
return c.MapClaims
}
// MarshalJSON marshals the MapClaims struct
func (c *MapClaims) MarshalJSON() ([]byte, error) {
return json.Marshal(c.MapClaims)
}
// https://tools.ietf.org/html/rfc7519#page-11
type jwtHeader struct {
Algorithm string `json:"alg"`
Type string `json:"typ"`
}
// ParseWithClaims - parse the token string, valid methods.
func ParseWithClaims(tokenStr string, claims *MapClaims) error {
bufp := base64BufPool.Get().(*[]byte)
defer base64BufPool.Put(bufp)
signer, err := parseUnverifiedMapClaims(tokenStr, claims, *bufp)
if err != nil {
return err
}
i := strings.LastIndex(tokenStr, ".")
if i < 0 {
return jwtgo.ErrSignatureInvalid
}
n, err := base64Decode(tokenStr[i+1:], *bufp)
if err != nil {
return err
}
var ok bool
claims.Data, ok = claims.Lookup(claimData)
if !ok {
return jwtgo.NewValidationError("data missing",
jwtgo.ValidationErrorClaimsInvalid)
}
claims.Subject, ok = claims.Lookup(claimSub)
if !ok {
return jwtgo.NewValidationError("sub missing",
jwtgo.ValidationErrorClaimsInvalid)
}
hasher := hmac.New(signer.Hash.New, []byte(GetHmacJWTSecret()))
hasher.Write([]byte(tokenStr[:i]))
if !hmac.Equal((*bufp)[:n], hasher.Sum(nil)) {
return jwtgo.ErrSignatureInvalid
}
// Signature is valid, lets validate the claims for
// other fields such as expiry etc.
return claims.Valid()
}
// base64Decode returns the bytes represented by the base64 string s.
func base64Decode(s string, buf []byte) (int, error) {
return base64.RawURLEncoding.Decode(buf, []byte(s))
}
// ParseUnverifiedMapClaims - WARNING: Don't use this method unless you know what you're doing
//
// This method parses the token but doesn't validate the signature. It's only
// ever useful in cases where you know the signature is valid (because it has
// been checked previously in the stack) and you want to extract values from
// it.
func parseUnverifiedMapClaims(tokenString string, claims *MapClaims, buf []byte) (*SigningMethodHMAC, error) {
if strings.Count(tokenString, ".") != 2 {
return nil, jwtgo.ErrSignatureInvalid
}
i := strings.Index(tokenString, ".")
j := strings.LastIndex(tokenString, ".")
n, err := base64Decode(tokenString[:i], buf)
if err != nil {
return nil, &jwtgo.ValidationError{Inner: err, Errors: jwtgo.ValidationErrorMalformed}
}
var header = jwtHeader{}
var json = jsoniter.ConfigCompatibleWithStandardLibrary
if err = json.Unmarshal(buf[:n], &header); err != nil {
return nil, &jwtgo.ValidationError{Inner: err, Errors: jwtgo.ValidationErrorMalformed}
}
n, err = base64Decode(tokenString[i+1:j], buf)
if err != nil {
return nil, &jwtgo.ValidationError{Inner: err, Errors: jwtgo.ValidationErrorMalformed}
}
if err = json.Unmarshal(buf[:n], &claims.MapClaims); err != nil {
return nil, &jwtgo.ValidationError{Inner: err, Errors: jwtgo.ValidationErrorMalformed}
}
for _, signer := range hmacSigners {
if header.Algorithm == signer.Name {
return signer, nil
}
}
return nil, jwtgo.NewValidationError(fmt.Sprintf("signing method (%s) is unavailable.", header.Algorithm),
jwtgo.ValidationErrorUnverifiable)
}

80
pkg/auth/jwt_test.go Normal file
View File

@@ -0,0 +1,80 @@
// 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 (
"testing"
"github.com/minio/minio-go/v6/pkg/credentials"
"github.com/stretchr/testify/assert"
)
var audience = ""
var creds = &credentials.Value{
AccessKeyID: "fakeAccessKeyID",
SecretAccessKey: "fakeSecretAccessKey",
SessionToken: "fakeSessionToken",
SignerType: 0,
}
var goodToken = ""
var badToken = "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiRDMwYWE0ekQ1bWtFaFRyWm5yOWM3NWh0Yko0MkROOWNDZVQ5RHVHUkg1U25SR3RyTXZNOXBMdnlFSVJAAAE5eWxxekhYMXllck8xUXpzMlZzRVFKeUF2ZmpOaDkrTVdoUURWZ2FhK2R5emxzSjNpK0k1dUdoeW5DNWswUW83WEY0UWszY0RtUTdUQUVROVFEbWRKdjBkdVB5L25hQk5vM3dIdlRDZHFNRDJZN3kycktJbmVUbUlFNmVveW9EWmprcW5tckVoYmMrTlhTRU81WjZqa1kwZ1E2eXZLaWhUZGxBRS9zS1lBNlc4Q1R1cm1MU0E0b0dIcGtldFZWU0VXMHEzNU9TU1VaczRXNkxHdGMxSTFWVFZLWUo3ZTlHR2REQ3hMWGtiZHQwcjl0RDNMWUhWRndra0dSZit5ZHBzS1Y3L1Jtbkp3SHNqNVVGV0w5WGVHUkZVUjJQclJTN2plVzFXeGZuYitVeXoxNVpOMzZsZ01GNnBlWFd1LzJGcEtrb2Z2QzNpY2x5Rmp0SE45ZkxYTVpVSFhnV2lsQWVSa3oiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjkwMDAiLCJleHAiOjE1ODc1MTY1NzEsInN1YiI6ImZmYmY4YzljLTJlMjYtNGMwYS1iMmI0LTYyMmVhM2I1YjZhYiJ9.P392RUwzsrBeJOO3fS1xMZcF-lWiDvWZ5hM7LZOyFMmoG5QLccDU5eAPSm8obzPoznX1b7eCFLeEmKK-vKgjiQ"
func TestNewJWTWithClaimsForClient(t *testing.T) {
funcAssert := assert.New(t)
// Test-1 : NewJWTWithClaimsForClient() is generated correctly without errors
function := "NewJWTWithClaimsForClient()"
jwt, err := NewJWTWithClaimsForClient(creds, 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 {
funcAssert.Equal("provided credentials are empty", err.Error())
}
}
func TestJWTAuthenticate(t *testing.T) {
funcAssert := assert.New(t)
// Test-1 : JWTAuthenticate() should correctly return the claims
function := "JWTAuthenticate()"
claims, err := JWTAuthenticate(goodToken)
if err != nil || claims == nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err)
} else {
funcAssert.Equal(claims.AccessKeyID, creds.AccessKeyID)
funcAssert.Equal(claims.SecretAccessKey, creds.SecretAccessKey)
funcAssert.Equal(claims.SessionToken, creds.SessionToken)
}
// 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())
}
// Test-3 : JWTAuthenticate() return an error because of an empty jwt
if _, err := JWTAuthenticate(""); err != nil {
funcAssert.Equal("JWT token missing", err.Error())
}
}
func TestIsJWTValid(t *testing.T) {
funcAssert := assert.New(t)
// Test-1 : JWTAuthenticate() provided token is valid
funcAssert.Equal(true, IsJWTValid(goodToken))
// Test-2 : JWTAuthenticate() provided token is invalid
funcAssert.Equal(false, IsJWTValid(badToken))
}

View File

@@ -14,52 +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/>.
package sessions
package utils
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"io"
"strings"
"sync"
mcCmd "github.com/minio/mc/cmd"
)
type Singleton struct {
sessions map[string]*mcCmd.Config
}
var instance *Singleton
var once sync.Once
// Returns a Singleton instance that keeps the sessions
func GetInstance() *Singleton {
once.Do(func() {
//build sessions hash
sessions := make(map[string]*mcCmd.Config)
instance = &Singleton{
sessions: sessions,
}
})
return instance
}
func (s *Singleton) NewSession(cfg *mcCmd.Config) string {
// genereate random session id
sessionID := RandomCharString(64)
// store the cfg under that session id
s.sessions[sessionID] = cfg
return sessionID
}
func (s *Singleton) ValidSession(sessionID string) bool {
if _, ok := s.sessions[sessionID]; ok {
return true
}
return false
}
// Do not use:
// https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-go
// It relies on math/rand and therefore not on a cryptographically secure RNG => It must not be used
@@ -87,3 +52,9 @@ func RandomCharString(n int) string {
}
return s.String()
}
func ComputeHmac256(message string, key []byte) string {
h := hmac.New(sha256.New, key)
h.Write([]byte(message))
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}

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/>.
package utils
import (
"crypto/sha1"
"testing"
"github.com/stretchr/testify/assert"
"golang.org/x/crypto/pbkdf2"
)
func TestRandomCharString(t *testing.T) {
funcAssert := assert.New(t)
// Test-1 : RandomCharString() should return string with expected length
length := 32
token := RandomCharString(length)
funcAssert.Equal(length, len(token))
// Test-2 : RandomCharString() should output random string, new generated string should not be equal to the previous one
newToken := RandomCharString(length)
funcAssert.NotEqual(token, newToken)
}
func TestComputeHmac256(t *testing.T) {
funcAssert := assert.New(t)
// Test-1 : ComputeHmac256() should return the right Hmac256 string based on a derived key
var derivedKey = pbkdf2.Key([]byte("secret"), []byte("salt"), 4096, 32, sha1.New)
var message = "hello world"
var expectedHmac = "5r32q7W+0hcBnqzQwJJUDzVGoVivXGSodTcHSqG/9Q8="
hmac := ComputeHmac256(message, derivedKey)
funcAssert.Equal(hmac, expectedHmac)
}

56
pkg/ws/websocket.go Normal file
View File

@@ -0,0 +1,56 @@
// 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 ws contains websocket utils for mcs project
package ws
import (
"net/http"
"strings"
"github.com/go-openapi/errors"
"github.com/minio/mcs/pkg/auth"
)
// Authenticate validates websocket header and returns mcs jwt claims
//
// Authorization Header needs to be like "Authorization Bearer <jwt_token>"
func Authenticate(r *http.Request) (*auth.DecryptedClaims, error) {
// Get Auth token
var reqToken string
// Token might come either as a Cookie or as a Header
// if not set in cookie, check if it is set on Header.
tokenCookie, err := r.Cookie("token")
if err != nil {
headerToken := r.Header.Get("Authorization")
// reqToken should come as "Bearer <token>"
splitHeaderToken := strings.Split(headerToken, "Bearer")
if len(splitHeaderToken) <= 1 {
return nil, errors.New(http.StatusBadRequest, "Authentication not valid")
}
reqToken = strings.TrimSpace(splitHeaderToken[1])
} 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
}

File diff suppressed because one or more lines are too long

View File

@@ -5,7 +5,7 @@
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.7.4",
"@babel/plugin-transform-react-jsx-development": "^7.9.0",
"@material-ui/core": "^4.9.8",
"@material-ui/core": "^4.9.12",
"@material-ui/icons": "^4.9.1",
"@types/history": "^4.7.3",
"@types/jest": "24.0.23",
@@ -19,6 +19,7 @@
"@types/recharts": "^1.8.9",
"@types/superagent": "^4.1.4",
"@types/webpack-env": "^1.14.1",
"@types/websocket": "^1.0.0",
"codemirror": "^5.52.2",
"history": "^4.10.1",
"local-storage-fallback": "^4.1.1",
@@ -37,7 +38,8 @@
"redux-thunk": "^2.3.0",
"superagent": "^5.1.0",
"typeface-roboto": "^0.0.75",
"typescript": "3.6.4"
"typescript": "3.6.4",
"websocket": "^1.0.31"
},
"scripts": {
"start": "PORT=5000 react-scripts start",
@@ -60,7 +62,7 @@
"last 1 safari version"
]
},
"proxy": "http://localhost:9090",
"proxy": "http://localhost:9090/",
"devDependencies": {
"prettier": "^1.19.1"
}

BIN
portal-ui/public/amqp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -12,7 +12,7 @@
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link href="https://fonts.googleapis.com/css?family=Questrial&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Lato:wght@400;500;700;900&display=swap" rel="stylesheet">
<link rel="apple-touch-icon" sizes="180x180" href="%PUBLIC_URL%/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="%PUBLIC_URL%/favicon-96x96.png">

BIN
portal-ui/public/kafka.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
portal-ui/public/mqtt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
portal-ui/public/mysql.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
portal-ui/public/nats.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

BIN
portal-ui/public/redis.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -15,15 +15,16 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { Route, Router, Switch } from "react-router-dom";
import { Redirect, Route, Router, Switch } from "react-router-dom";
import history from "./history";
import Login from "./screens/LoginPage";
import Login from "./screens/LoginPage/LoginPage";
import Console from "./screens/Console/Console";
import NotFoundPage from "./screens/NotFoundPage";
import storage from "local-storage-fallback";
import { connect } from "react-redux";
import { AppState } from "./store";
import { userLoggedIn } from "./actions";
import LoginCallback from "./screens/LoginPage/LoginCallback";
const isLoggedIn = () => {
return (
@@ -55,6 +56,7 @@ class Routes extends React.Component<RoutesProps> {
return (
<Router history={history}>
<Switch>
<Route exact path="/oauth_callback" component={LoginCallback} />
<Route exact path="/login" component={Login} />
{this.props.loggedIn ? (
<Switch>
@@ -64,7 +66,7 @@ class Routes extends React.Component<RoutesProps> {
) : (
<Switch>
<Route exact path="/" component={Login} />
<Route component={NotFoundPage} />
<Redirect to="/" />
</Switch>
)}
</Switch>

View File

@@ -14,7 +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 { MENU_OPEN, USER_LOGGED } from "./types";
import {
MENU_OPEN,
SERVER_IS_LOADING,
SERVER_NEEDS_RESTART,
USER_LOGGED
} from "./types";
export function userLoggedIn(loggedIn: boolean) {
return {
@@ -29,3 +34,17 @@ export function setMenuOpen(open: boolean) {
open: open
};
}
export function serverNeedsRestart(needsRestart: boolean) {
return {
type: SERVER_NEEDS_RESTART,
needsRestart: needsRestart
};
}
export function serverIsLoading(isLoading: boolean) {
return {
type: SERVER_IS_LOADING,
isLoading: isLoading
};
}

View File

@@ -24,7 +24,14 @@ export class API {
.set("Authorization", `Bearer ${token}`)
.send(data)
.then(res => res.body)
.catch(err => this.onError(err));
.catch(err => {
// if we get unauthorized, kick out the user
if (err.status === 401) {
storage.removeItem("token");
window.location.href = "/";
}
return this.onError(err);
});
}
onError(err: any) {
@@ -38,5 +45,6 @@ export class API {
}
}
}
const api = new API();
export default api;

View File

@@ -0,0 +1,40 @@
// 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 units = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
export const niceBytes = (x: string) => {
let l = 0,
n = parseInt(x, 10) || 0;
while (n >= 1024 && ++l) {
n = n / 1024;
}
//include a decimal point and a tenths-place digit if presenting
//less than ten of KB or greater units
return n.toFixed(n < 10 && l > 0 ? 1 : 0) + " " + units[l];
};
export const setCookie = (name: string, val: string) => {
const date = new Date();
const value = val;
// Set it expire in 45 minutes
date.setTime(date.getTime() + 45 * 60 * 1000);
// Set it
document.cookie =
name + "=" + value + "; expires=" + date.toUTCString() + "; path=/";
};

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 81.879 23.119"><defs><style>.cls-1{fill:#1b1556;}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M10.086,13.4,8.969,14.573a4.2,4.2,0,0,0-3.01-1.279,3.886,3.886,0,0,0-3.884,4.093,3.888,3.888,0,0,0,3.884,4.1A4.4,4.4,0,0,0,9,20.2l1.082,1.186A5.344,5.344,0,0,1,6,23.119a5.512,5.512,0,0,1-5.7-5.732A5.509,5.509,0,0,1,6,11.667,5.328,5.328,0,0,1,10.086,13.4Z"/><path class="cls-1" d="M23.608,17.387a5.637,5.637,0,0,1-5.8,5.732,5.628,5.628,0,0,1-5.791-5.732,5.626,5.626,0,0,1,5.791-5.72A5.635,5.635,0,0,1,23.608,17.387Zm-9.825,0a4.024,4.024,0,1,0,8.046,0,4.024,4.024,0,1,0-8.046,0Z"/><path class="cls-1" d="M36.862,16.073V22.9H35.129V16.643c0-2.093-1.163-3.325-3.174-3.325a3.24,3.24,0,0,0-3.371,3.372V22.9H26.839V11.888H28.56v1.569a4.354,4.354,0,0,1,3.709-1.79A4.261,4.261,0,0,1,36.862,16.073Z"/><path class="cls-1" d="M48.2,14.225a6.872,6.872,0,0,0-3.605-1.035c-1.569,0-2.6.663-2.6,1.732,0,.919.8,1.372,2.244,1.547l1.3.163c2.338.3,3.7,1.267,3.7,3.069,0,2.093-1.884,3.407-4.849,3.407A7.725,7.725,0,0,1,39.791,21.7l.8-1.3a5.8,5.8,0,0,0,3.815,1.2c1.86,0,3.034-.616,3.034-1.778,0-.884-.744-1.419-2.3-1.605l-1.314-.151c-2.477-.3-3.639-1.408-3.639-3.046,0-2.082,1.755-3.338,4.4-3.338a8.067,8.067,0,0,1,4.372,1.2Z"/><path class="cls-1" d="M63.033,17.387a5.8,5.8,0,0,1-11.593,0,5.8,5.8,0,0,1,11.593,0Zm-9.825,0a4.023,4.023,0,1,0,8.045,0,4.023,4.023,0,1,0-8.045,0Z"/><path class="cls-1" d="M68.008,22.9H66.264V6.155h1.744Z"/><path class="cls-1" d="M81.879,17.353a5.606,5.606,0,0,1-.035.65H73.019a3.743,3.743,0,0,0,3.9,3.593A5.1,5.1,0,0,0,80.4,20.213l.931,1.186a6.179,6.179,0,0,1-4.524,1.72A5.394,5.394,0,0,1,71.24,17.4a5.406,5.406,0,0,1,5.465-5.732C79.693,11.667,81.856,14,81.879,17.353ZM73.043,16.6h7.069a3.446,3.446,0,0,0-3.442-3.384A3.59,3.59,0,0,0,73.043,16.6Z"/><rect class="cls-1" x="13.484" y="0.12" width="2.328" height="6.875"/><path class="cls-1" d="M10.662.215,5.936,3.1a.21.21,0,0,1-.219,0L.992.215A.651.651,0,0,0,.654.12H.648A.648.648,0,0,0,0,.768v6.22H2.327V4.028a.233.233,0,0,1,.354-.2l2.648,1.62a.829.829,0,0,0,.853.008l2.8-1.639a.232.232,0,0,1,.35.2V6.988h2.327V.768A.648.648,0,0,0,11.006.12H11A.651.651,0,0,0,10.662.215Z"/><path class="cls-1" d="M27.422.12H25.061V3.25a.233.233,0,0,1-.342.205L18.6.2A.662.662,0,0,0,18.3.12h0a.648.648,0,0,0-.648.648v6.22h2.342V3.863a.233.233,0,0,1,.342-.206L26.47,6.915a.646.646,0,0,0,.3.076h0a.648.648,0,0,0,.648-.648Z"/><path class="cls-1" d="M29.252,7V.12h1.072V7Z"/><path class="cls-1" d="M36.629,7.119c-2.882,0-4.927-1.368-4.927-3.56S33.759,0,36.629,0s4.938,1.367,4.938,3.559S39.547,7.119,36.629,7.119Zm0-6.208c-2.143,0-3.794.936-3.794,2.648s1.651,2.648,3.794,2.648,3.805-.923,3.805-2.648S38.772.911,36.629.911Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

View File

@@ -1,8 +1,6 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
font-family: 'Lato', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@@ -14,13 +14,22 @@
// 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 {MENU_OPEN, SystemActionTypes, SystemState, USER_LOGGED} from "./types";
import {
MENU_OPEN,
SERVER_IS_LOADING,
SERVER_NEEDS_RESTART,
SystemActionTypes,
SystemState,
USER_LOGGED
} from "./types";
const initialState: SystemState = {
loggedIn: false,
session: "",
userName: "",
sidebarOpen:true,
sidebarOpen: true,
serverNeedsRestart: false,
serverIsLoading: false
};
export function systemReducer(
@@ -38,6 +47,17 @@ export function systemReducer(
...state,
sidebarOpen: action.open
};
case SERVER_NEEDS_RESTART:
return {
...state,
serverNeedsRestart: action.needsRestart
};
case SERVER_IS_LOADING:
return {
...state,
serverIsLoading: action.isLoading
};
default:
return state;
}

View File

@@ -35,9 +35,8 @@ import { AppState } from "../../../store";
import { setMenuOpen } from "../../../actions";
import { ThemedComponentProps } from "@material-ui/core/styles/withTheme";
import NotFoundPage from "../../NotFoundPage";
import BucketList from "./ListBuckets/ListBuckets";
import ViewBucket from "./ViewBucket/ViewBucket";
import ListBuckets from "./ListBuckets/ListBuckets";
import ViewBucket from "./ViewBucket/ViewBucket";
const styles = (theme: Theme) =>
createStyles({

View File

@@ -16,27 +16,20 @@
import React from "react";
import Grid from "@material-ui/core/Grid";
import Title from "../../../../common/Title";
import Typography from "@material-ui/core/Typography";
import {
Button,
Dialog,
DialogContent,
DialogTitle,
FormControl,
InputLabel,
LinearProgress,
MenuItem,
Select,
TextField
} from "@material-ui/core";
import { Button, LinearProgress } from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import api from "../../../../common/api";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
},
buttonContainer: {
textAlign: "right"
}
});
@@ -50,28 +43,25 @@ interface IAddBucketState {
addLoading: boolean;
addError: string;
bucketName: string;
accessPolicy: string;
}
class AddBucket extends React.Component<IAddBucketProps, IAddBucketState> {
state: IAddBucketState = {
addLoading: false,
addError: "",
bucketName: "",
accessPolicy: ""
bucketName: ""
};
addRecord(event: React.FormEvent) {
event.preventDefault();
const { bucketName, addLoading, accessPolicy } = this.state;
const { bucketName, addLoading } = this.state;
if (addLoading) {
return;
}
this.setState({ addLoading: true }, () => {
api
.invoke("POST", "/api/v1/buckets", {
name: bucketName,
access: accessPolicy
name: bucketName
})
.then(res => {
this.setState(
@@ -95,10 +85,11 @@ class AddBucket extends React.Component<IAddBucketProps, IAddBucketState> {
render() {
const { classes, open } = this.props;
const { addLoading, addError, accessPolicy } = this.state;
const { addLoading, addError, bucketName } = this.state;
return (
<Dialog
open={open}
<ModalWrapper
title="Create Bucket"
modalOpen={open}
onClose={() => {
this.setState({ addError: "" }, () => {
this.props.closeModalAndRefresh();
@@ -107,81 +98,58 @@ class AddBucket extends React.Component<IAddBucketProps, IAddBucketState> {
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">
<Title>Create Bucket</Title>
</DialogTitle>
<DialogContent>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
this.addRecord(e);
}}
>
<Grid container>
{addError !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
</Grid>
)}
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
this.addRecord(e);
}}
>
<Grid container>
{addError !== "" && (
<Grid item xs={12}>
<TextField
id="standard-basic"
fullWidth
label="Bucket Name"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ bucketName: e.target.value });
}}
/>
</Grid>
<Grid item xs={12}>
<FormControl className={classes.formControl} fullWidth>
<InputLabel id="select-access-policy">
Access Policy
</InputLabel>
<Select
labelId="select-access-policy"
id="select-access-policy"
value={accessPolicy}
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
this.setState({ accessPolicy: e.target.value as string });
}}
>
<MenuItem value="PRIVATE">Private</MenuItem>
<MenuItem value="PUBLIC">Public</MenuItem>
<MenuItem value="CUSTOM">Custom</MenuItem>
</Select>
</FormControl>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<Button
type="submit"
variant="contained"
color="primary"
fullWidth
disabled={addLoading}
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
Save
</Button>
{addError}
</Typography>
</Grid>
{addLoading && (
<Grid item xs={12}>
<LinearProgress />
</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>
</form>
</DialogContent>
</Dialog>
<Grid item xs={12}>
<br />
<br />
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>
<Button
type="submit"
variant="contained"
color="primary"
disabled={addLoading}
>
Save
</Button>
</Grid>
{addLoading && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid>
</form>
</ModalWrapper>
);
}
}

View File

@@ -16,34 +16,20 @@
import React 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 { Bucket, BucketList } from "../types";
import {
Button,
IconButton,
LinearProgress,
TableFooter,
TablePagination
} from "@material-ui/core";
import { Button } from "@material-ui/core";
import Typography from "@material-ui/core/Typography";
import DeleteIcon from "@material-ui/icons/Delete";
import AddBucket from "./AddBucket";
import DeleteBucket from "./DeleteBucket";
import { MinTablePaginationActions } from "../../../../common/MinTablePaginationActions";
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 Moment from "react-moment";
import { Link } from "react-router-dom";
import ViewIcon from "@material-ui/icons/Visibility";
import api from "../../../../common/api";
import { Bucket, BucketList } from "../types";
import TableWrapper from "../../Common/TableWrapper/TableWrapper";
import AddBucket from "./AddBucket";
import DeleteBucket from "./DeleteBucket";
import { MinTablePaginationActions } from "../../../../common/MinTablePaginationActions";
import { CreateIcon } from "../../../../icons";
const styles = (theme: Theme) =>
createStyles({
@@ -134,8 +120,8 @@ class ListBuckets extends React.Component<
.then((res: BucketList) => {
this.setState({
loading: false,
records: res.buckets,
totalRecords: res.total,
records: res.buckets || [],
totalRecords: !res.buckets ? 0 : res.total,
error: ""
});
// if we get 0 results, and page > 0 , go down 1 page
@@ -208,14 +194,48 @@ class ListBuckets extends React.Component<
this.setState({ deleteOpen: true, selectedBucket: bucket });
};
const tableActions = [
{ type: "view", to: `/buckets`, sendOnlyId: true },
{ type: "delete", onClick: confirmDeleteBucket, sendOnlyId: true }
];
const displayParsedDate = (date: string) => {
return <Moment>{date}</Moment>;
};
const filteredRecords = records
.slice(offset, offset + rowsPerPage)
.filter((b: Bucket) => {
if (filterBuckets === "") {
return true;
} else {
if (b.name.indexOf(filterBuckets) >= 0) {
return true;
} else {
return false;
}
}
});
return (
<React.Fragment>
<AddBucket
open={addScreenOpen}
closeModalAndRefresh={() => {
this.closeAddModalAndRefresh();
}}
/>
{addScreenOpen && (
<AddBucket
open={addScreenOpen}
closeModalAndRefresh={() => {
this.closeAddModalAndRefresh();
}}
/>
)}
{deleteOpen && (
<DeleteBucket
deleteOpen={deleteOpen}
selectedBucket={selectedBucket}
closeDeleteModalAndRefresh={(refresh: boolean) => {
this.closeDeleteModalAndRefresh(refresh);
}}
/>
)}
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">Buckets</Typography>
@@ -260,88 +280,37 @@ class ListBuckets extends React.Component<
<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>Name</TableCell>
<TableCell>Creation Date</TableCell>
<TableCell align="right">Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{records
.slice(offset, offset + rowsPerPage)
.filter((b: Bucket) => {
if (filterBuckets === "") {
return true;
} else {
if (b.name.indexOf(filterBuckets) >= 0) {
return true;
} else {
return false;
}
}
})
.map(row => (
<TableRow key={row.name}>
<TableCell>{row.name}</TableCell>
<TableCell>
<Moment>{row.creation_date}</Moment>
</TableCell>
<TableCell align="right">
<Link to={`/buckets/${row.name}`}>
<IconButton aria-label="delete">
<ViewIcon />
</IconButton>
</Link>
<IconButton
aria-label="delete"
onClick={() => {
confirmDeleteBucket(row.name);
}}
>
<DeleteIcon />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={[5, 10, 25]}
colSpan={3}
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 Buckets</div>
)}
</Paper>
<TableWrapper
itemActions={tableActions}
columns={[
{ label: "Name", elementKey: "name" },
{
label: "Creation Date",
elementKey: "creation_date",
renderFunction: displayParsedDate
}
]}
isLoading={loading}
records={filteredRecords}
entityName="Buckets"
idField="name"
paginatorConfig={{
rowsPerPageOptions: [5, 10, 25],
colSpan: 3,
count: totalRecords,
rowsPerPage: rowsPerPage,
page: page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true
},
onChangePage: handleChangePage,
onChangeRowsPerPage: handleChangeRowsPerPage,
ActionsComponent: MinTablePaginationActions
}}
/>
</Grid>
</Grid>
<DeleteBucket
deleteOpen={deleteOpen}
selectedBucket={selectedBucket}
closeDeleteModalAndRefresh={(refresh: boolean) => {
this.closeDeleteModalAndRefresh(refresh);
}}
/>
</React.Fragment>
);
}

View File

@@ -16,20 +16,8 @@
import React, { ChangeEvent } from "react";
import Grid from "@material-ui/core/Grid";
import Title from "../../../../common/Title";
import Typography from "@material-ui/core/Typography";
import {
Button,
Dialog,
DialogContent,
DialogTitle,
FormControl,
InputLabel,
LinearProgress,
MenuItem,
Select,
TextField
} from "@material-ui/core";
import { Button, LinearProgress } from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import api from "../../../../common/api";
import TableHead from "@material-ui/core/TableHead";
@@ -38,7 +26,10 @@ import TableCell from "@material-ui/core/TableCell";
import TableBody from "@material-ui/core/TableBody";
import Checkbox from "@material-ui/core/Checkbox";
import Table from "@material-ui/core/Table";
import { ArnList, BucketEventList } from "../types";
import { ArnList } from "../types";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
const styles = (theme: Theme) =>
createStyles({
@@ -52,6 +43,9 @@ const styles = (theme: Theme) =>
fontWeight: "bold"
}
}
},
buttonContainer: {
textAlign: "right"
}
});
@@ -97,7 +91,7 @@ class AddEvent extends React.Component<IAddEventProps, IAddEventState> {
arn: arn,
events: selectedEvents,
prefix: prefix,
sufix: suffix
suffix: suffix
},
ignoreExisting: true
})
@@ -148,7 +142,15 @@ class AddEvent extends React.Component<IAddEventProps, IAddEventState> {
render() {
const { classes, open } = this.props;
const { addLoading, addError, arn, selectedEvents, arnList } = this.state;
const {
addLoading,
addError,
arn,
selectedEvents,
arnList,
prefix,
suffix
} = this.state;
const events = [
{ label: "PUT - Object Uploaded", value: "put" },
@@ -156,26 +158,6 @@ class AddEvent extends React.Component<IAddEventProps, IAddEventState> {
{ label: "DELETE - Object Deleted", value: "delete" }
];
const selectionChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
const targetD = e.target;
const value = targetD.value;
const checked = targetD.checked;
let elements: string[] = [...selectedEvents]; // We clone the selectedGroups array
if (checked) {
// If the user has checked this field we need to push this to selectedGroupsList
elements.push(value);
} else {
// User has unchecked this field, we need to remove it from the list
elements = elements.filter(element => element !== value);
}
this.setState({ selectedEvents: selectedEvents });
return elements;
};
const handleClick = (
event: React.MouseEvent<unknown> | ChangeEvent<unknown>,
name: string
@@ -199,134 +181,131 @@ class AddEvent extends React.Component<IAddEventProps, IAddEventState> {
this.setState({ selectedEvents: newSelected });
};
const arnValues = arnList.map(arnConstant => ({
label: arnConstant,
value: arnConstant
}));
return (
<Dialog
open={open}
<ModalWrapper
modalOpen={open}
onClose={() => {
this.setState({ addError: "" }, () => {
this.props.closeModalAndRefresh();
});
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
title="Subscribe To Event"
>
<DialogTitle id="alert-dialog-title">
<Title>Subscribe To Event</Title>
</DialogTitle>
<DialogContent>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
this.addRecord(e);
}}
>
<Grid container>
{addError !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
</Grid>
)}
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
this.addRecord(e);
}}
>
<Grid container>
{addError !== "" && (
<Grid item xs={12}>
<FormControl className={classes.formControl} fullWidth>
<InputLabel id="select-access-policy">ARN</InputLabel>
<Select
labelId="select-access-policy"
id="select-access-policy"
value={arn}
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
this.setState({ arn: e.target.value as string });
}}
>
{arnList.map(arn => (
<MenuItem value={arn}>{arn}</MenuItem>
))}
</Select>
</FormControl>
</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>
</TableRow>
))}
</TableBody>
</Table>
</Grid>
<Grid item xs={12}>
<TextField
id="standard-basic"
fullWidth
label="Prefix"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ prefix: e.target.value });
}}
/>
</Grid>
<Grid item xs={12}>
<TextField
id="standard-basic"
fullWidth
label="Suffix"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ suffix: e.target.value });
}}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<Button
type="submit"
variant="contained"
color="primary"
fullWidth
disabled={addLoading}
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
Save
</Button>
{addError}
</Typography>
</Grid>
{addLoading && (
<Grid item xs={12}>
<LinearProgress />
</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>
</form>
</DialogContent>
</Dialog>
<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>
</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 item xs={12} className={classes.buttonContainer}>
<Button
type="submit"
variant="contained"
color="primary"
disabled={addLoading}
>
Save
</Button>
</Grid>
{addLoading && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid>
</form>
</ModalWrapper>
);
}
}

View File

@@ -67,6 +67,7 @@ class DeleteEvent extends React.Component<
if (bucketEvent == null) {
return;
}
this.setState({ deleteLoading: true }, () => {
api
.invoke(
@@ -97,7 +98,7 @@ class DeleteEvent extends React.Component<
}
render() {
const { classes, deleteOpen, selectedBucket } = this.props;
const { classes, deleteOpen } = this.props;
const { deleteLoading, deleteError } = this.state;
return (

View File

@@ -15,22 +15,12 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import Grid from "@material-ui/core/Grid";
import Title from "../../../../common/Title";
import Typography from "@material-ui/core/Typography";
import {
Button,
Dialog,
DialogContent,
DialogTitle,
FormControl,
InputLabel,
LinearProgress,
MenuItem,
Select,
TextField
} from "@material-ui/core";
import { Button, LinearProgress } from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import api from "../../../../common/api";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
const styles = (theme: Theme) =>
createStyles({
@@ -98,81 +88,71 @@ class SetAccessPolicy extends React.Component<
const { classes, open } = this.props;
const { addLoading, addError, accessPolicy } = this.state;
return (
<Dialog
open={open}
<ModalWrapper
title="Change Access Policy"
modalOpen={open}
onClose={() => {
this.setState({ addError: "" }, () => {
this.props.closeModalAndRefresh();
});
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">
<Title>Change Access Policy</Title>
</DialogTitle>
<DialogContent>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
this.addRecord(e);
}}
>
<Grid container>
{addError !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
</Grid>
)}
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
this.addRecord(e);
}}
>
<Grid container>
{addError !== "" && (
<Grid item xs={12}>
<FormControl className={classes.formControl} fullWidth>
<InputLabel id="select-access-policy">
Access Policy
</InputLabel>
<Select
labelId="select-access-policy"
id="select-access-policy"
value={accessPolicy}
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
this.setState({ accessPolicy: e.target.value as string });
}}
>
<MenuItem value="PRIVATE">Private</MenuItem>
<MenuItem value="PUBLIC">Public</MenuItem>
<MenuItem value="CUSTOM">Custom</MenuItem>
</Select>
</FormControl>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<Button
type="submit"
variant="contained"
color="primary"
fullWidth
disabled={addLoading}
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
Set
</Button>
{addError}
</Typography>
</Grid>
{addLoading && (
<Grid item xs={12}>
<LinearProgress />
</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>
</form>
</DialogContent>
</Dialog>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<Button
type="submit"
variant="contained"
color="primary"
fullWidth
disabled={addLoading}
>
Set
</Button>
</Grid>
{addLoading && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid>
</form>
</ModalWrapper>
);
}
}

View File

@@ -15,31 +15,19 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import get from "lodash/get";
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 { BucketEvent, BucketEventList, BucketInfo } from "../types";
import {
Button,
IconButton,
LinearProgress,
TableFooter,
TablePagination
} from "@material-ui/core";
import { Button } from "@material-ui/core";
import Typography from "@material-ui/core/Typography";
import DeleteIcon from "@material-ui/icons/Delete";
import SetAccessPolicy from "./SetAccessPolicy";
import DeleteBucket from "../ListBuckets/DeleteBucket";
import { MinTablePaginationActions } from "../../../../common/MinTablePaginationActions";
import { CreateIcon } from "../../../../icons";
import AddEvent from "./AddEvent";
import DeleteEvent from "./DeleteEvent";
import TableWrapper from "../../Common/TableWrapper/TableWrapper";
const styles = (theme: Theme) =>
createStyles({
@@ -130,25 +118,23 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
fetchEvents() {
this.setState({ loading: true }, () => {
const { page, rowsPerPage } = this.state;
const { page } = this.state;
const { match } = this.props;
const bucketName = match.params["bucketName"];
api
.invoke("GET", `/api/v1/buckets/${bucketName}/events`)
.then((res: BucketEventList) => {
const events = get(res, "events", []);
const total = get(res, "total", 0);
this.setState({
loading: false,
records: res.events,
totalRecords: res.total,
records: events || [],
totalRecords: total,
error: ""
});
// if we get 0 results, and page > 0 , go down 1 page
if (
(res.events === undefined ||
res.events == null ||
res.events.length === 0) &&
page > 0
) {
if ((!events || res.events.length === 0) && page > 0) {
const newPage = page - 1;
this.setState({ page: newPage }, () => {
this.fetchEvents();
@@ -205,7 +191,6 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
rowsPerPage,
deleteOpen,
addScreenOpen,
selectedBucket,
selectedEvent
} = this.state;
@@ -233,6 +218,14 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
accessPolicy = info.access;
}
const eventsDisplay = (events: string[]) => {
return <React.Fragment>{events.join(", ")}</React.Fragment>;
};
const tableActions = [{ type: "delete", onClick: confirmDeleteEvent }];
const filteredRecords = records.slice(offset, offset + rowsPerPage);
return (
<React.Fragment>
<AddEvent
@@ -303,62 +296,37 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
<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>SQS</TableCell>
<TableCell>Events</TableCell>
<TableCell>Prefix</TableCell>
<TableCell>Suffix</TableCell>
<TableCell align="right">Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{records.slice(offset, offset + rowsPerPage).map(row => (
<TableRow key={row.id}>
<TableCell>{row.arn}</TableCell>
<TableCell>{row.events.join(", ")}</TableCell>
<TableCell>{row.prefix}</TableCell>
<TableCell>{row.suffix}</TableCell>
<TableCell align="right">
<IconButton
aria-label="delete"
onClick={() => {
confirmDeleteEvent(row);
}}
>
<DeleteIcon />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={[5, 10, 25]}
colSpan={3}
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 className={classes.noRecords}>No Events</div>
)}
</Paper>
<TableWrapper
itemActions={tableActions}
columns={[
{ label: "SQS", elementKey: "arn" },
{
label: "Events",
elementKey: "events",
renderFunction: eventsDisplay
},
{ label: "Prefix", elementKey: "prefix" },
{ label: "Suffix", elementKey: "suffix" }
]}
isLoading={loading}
records={filteredRecords}
entityName="Events"
idField="id"
paginatorConfig={{
rowsPerPageOptions: [5, 10, 25],
colSpan: 3,
count: totalRecords,
rowsPerPage: rowsPerPage,
page: page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true
},
onChangePage: handleChangePage,
onChangeRowsPerPage: handleChangeRowsPerPage,
ActionsComponent: MinTablePaginationActions
}}
/>
</Grid>
</Grid>

View File

@@ -0,0 +1,151 @@
// This file is part of MinIO Console Server
// Copyright (c) 2019 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, {
useState,
useEffect,
createRef,
ChangeEvent,
useCallback
} 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 HelpIcon from "@material-ui/icons/Help";
interface ICSVMultiSelector {
elements: string;
name: string;
label: string;
tooltip?: string;
classes: any;
onChange: (elements: string) => void;
}
const styles = (theme: Theme) =>
createStyles({
...fieldBasic,
inputLabel: {
...fieldBasic.inputLabel,
marginBottom: 10
},
inputContainer: {
height: 150,
overflowY: "auto",
padding: 15,
position: "relative",
border: "1px solid #c4c4c4",
marginBottom: 10
},
labelContainer: {
display: "flex"
}
});
const CSVMultiSelector = ({
elements,
name,
label,
tooltip = "",
onChange,
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);
}
}, [elements, setCurrentElements, currentElements]);
// Use effect to send new values to onChange
useEffect(() => {
const elementsString = currentElements
.filter(element => element.trim() !== "")
.join(",");
onChange(elementsString);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentElements]);
// If the last input is not empty, we add a new one
const addEmptyLine = (elementsUp: string[]) => {
if (elementsUp[elementsUp.length - 1].trim() !== "") {
elementsUp.push("");
const refScroll = bottomList.current;
if (refScroll) {
refScroll.scrollIntoView(false);
}
}
return elementsUp;
};
// Onchange function for input box, we get the dataset-index & only update that value in the array
const onChangeElement = (e: ChangeEvent<HTMLInputElement>) => {
e.persist();
let updatedElement = [...currentElements];
const index = get(e.target, "dataset.index", 0);
updatedElement[index] = e.target.value;
updatedElement = addEmptyLine(updatedElement);
setCurrentElements(updatedElement);
};
const inputs = currentElements.map((element, index) => {
return (
<InputBoxWrapper
id={`${name}-${index.toString()}`}
label={""}
name={`${name}-${index.toString()}`}
value={currentElements[index]}
onChange={onChangeElement}
index={index}
key={`csv-${name}-${index.toString()}`}
/>
);
});
return (
<React.Fragment>
<Grid item xs={12} className={classes.fieldContainer}>
<InputLabel className={classes.inputLabel}>{label}</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>
);
};
export default withStyles(styles)(CSVMultiSelector);

View File

@@ -0,0 +1,132 @@
// 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 from "react";
import {
Grid,
InputLabel,
TextField,
TextFieldProps,
Tooltip
} from "@material-ui/core";
import { OutlinedInputProps } from "@material-ui/core/OutlinedInput";
import {
createStyles,
makeStyles,
Theme,
withStyles
} from "@material-ui/core/styles";
import { fieldBasic } from "../common/styleLibrary";
import HelpIcon from "@material-ui/icons/Help";
interface InputBoxProps {
label: string;
classes: any;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
value: string | boolean;
id: string;
name: string;
disabled?: boolean;
multiline?: boolean;
type?: string;
tooltip?: string;
autoComplete?: string;
index?: number;
}
const styles = (theme: Theme) =>
createStyles({
...fieldBasic,
textBoxContainer: {
flexGrow: 1
}
});
const inputStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
borderColor: "#393939",
borderRadius: 0
},
input: {
padding: "11px 20px",
color: "#393939",
fontSize: 14
}
})
);
function InputField(props: TextFieldProps) {
const classes = inputStyles();
return (
<TextField
InputProps={{ classes } as Partial<OutlinedInputProps>}
{...props}
/>
);
}
const InputBoxWrapper = ({
label,
onChange,
value,
id,
name,
type = "text",
autoComplete = "off",
disabled = false,
multiline = false,
tooltip = "",
index = 0,
classes
}: InputBoxProps) => {
return (
<React.Fragment>
<Grid item xs={12} className={classes.fieldContainer}>
{label !== "" && (
<InputLabel htmlFor={id} className={classes.inputLabel}>
{label}
</InputLabel>
)}
<div className={classes.textBoxContainer}>
<InputField
className={classes.boxDesign}
id={id}
name={name}
variant="outlined"
fullWidth
value={value}
disabled={disabled}
onChange={onChange}
type={type}
multiline={multiline}
autoComplete={autoComplete}
inputProps={{ "data-index": index }}
/>
</div>
{tooltip !== "" && (
<div className={classes.tooltipContainer}>
<Tooltip title={tooltip} placement="left">
<HelpIcon />
</Tooltip>
</div>
)}
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(InputBoxWrapper);

View File

@@ -0,0 +1,154 @@
// 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 from "react";
import Grid from "@material-ui/core/Grid";
import RadioGroup from "@material-ui/core/RadioGroup";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Radio, { RadioProps } from "@material-ui/core/Radio";
import { InputLabel, Tooltip } from "@material-ui/core";
import {
createStyles,
Theme,
withStyles,
makeStyles
} from "@material-ui/core/styles";
import { fieldBasic } from "../common/styleLibrary";
import HelpIcon from "@material-ui/icons/Help";
export interface SelectorTypes {
label: string;
value: string;
}
interface RadioGroupProps {
selectorOptions: SelectorTypes[];
currentSelection: string;
label: string;
id: string;
name: string;
tooltip?: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
classes: any;
displayInColumn?: boolean;
}
const styles = (theme: Theme) =>
createStyles({
...fieldBasic,
radioBoxContainer: {
flexGrow: 1
}
});
const radioStyles = makeStyles({
root: {
"&:hover": {
backgroundColor: "transparent"
}
},
icon: {
borderRadius: "100%",
width: 14,
height: 14,
border: "1px solid #000"
},
checkedIcon: {
borderRadius: "100%",
width: 14,
height: 14,
border: "1px solid #000",
padding: 4,
position: "relative",
"&::after": {
content: '" "',
width: 8,
height: 8,
borderRadius: "100%",
display: "block",
position: "absolute",
backgroundColor: "#000",
top: 2,
left: 2
}
}
});
const RadioButton = (props: RadioProps) => {
const classes = radioStyles();
return (
<Radio
className={classes.root}
disableRipple
color="default"
checkedIcon={<span className={classes.checkedIcon} />}
icon={<span className={classes.icon} />}
{...props}
/>
);
};
export const RadioGroupSelector = ({
selectorOptions = [],
currentSelection,
label,
id,
name,
onChange,
tooltip = "",
classes,
displayInColumn = false
}: RadioGroupProps) => {
return (
<React.Fragment>
<Grid item xs={12} className={classes.fieldContainer}>
<InputLabel htmlFor={id} className={classes.inputLabel}>
{label}
</InputLabel>
<div className={classes.radioBoxContainer}>
<RadioGroup
aria-label={id}
id={id}
name={name}
value={currentSelection}
onChange={onChange}
row={!displayInColumn}
>
{selectorOptions.map(selectorOption => {
return (
<FormControlLabel
key={`rd-${name}-${selectorOption.value}`}
value={selectorOption.value}
control={<RadioButton />}
label={selectorOption.label}
/>
);
})}
</RadioGroup>
</div>
{tooltip !== "" && (
<div>
<Tooltip title={tooltip} placement="left">
<HelpIcon />
</Tooltip>
</div>
)}
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(RadioGroupSelector);

View File

@@ -0,0 +1,112 @@
// 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 from "react";
import Grid from "@material-ui/core/Grid";
import {
FormControl,
InputLabel,
MenuItem,
Select,
InputBase
} from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { fieldBasic } from "../common/styleLibrary";
interface selectorTypes {
label: string;
value: string;
}
interface SelectProps {
options: selectorTypes[];
value: string;
label: string;
id: string;
name: string;
onChange: (
e: React.ChangeEvent<{ name?: string | undefined; value: unknown }>
) => void;
classes: any;
}
const styles = (theme: Theme) =>
createStyles({
...fieldBasic
});
const SelectStyled = withStyles((theme: Theme) =>
createStyles({
root: {
"label + &": {
marginTop: theme.spacing(3)
}
},
input: {
borderRadius: 0,
position: "relative",
color: "#393939",
fontSize: 14,
padding: "11px 20px",
border: "1px solid #c4c4c4",
"&:hover": {
borderColor: "#393939"
},
"&:focus": {
backgroundColor: "#fff"
}
}
})
)(InputBase);
const SelectWrapper = ({
classes,
id,
name,
onChange,
options,
label,
value
}: SelectProps) => {
return (
<React.Fragment>
<Grid item xs={12} className={classes.fieldContainer}>
<InputLabel htmlFor={id} className={classes.inputLabel}>
{label}
</InputLabel>
<FormControl variant="outlined" fullWidth>
<Select
id={id}
name={name}
value={value}
onChange={onChange}
input={<SelectStyled />}
>
{options.map(option => (
<MenuItem
value={option.value}
key={`select-${name}-${option.label}`}
>
{option.label}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(SelectWrapper);

View File

@@ -0,0 +1,35 @@
// 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/>.
// This object contains variables that will be used across form components.
export const fieldBasic = {
inputLabel: {
fontWeight: 500,
marginRight: 16,
minWidth: 90,
fontSize: 14,
color: "#393939"
},
fieldContainer: {
display: "flex",
alignItems: "center",
marginBottom: 10
},
tooltipContainer: {
marginLeft: 5
}
};

View File

@@ -0,0 +1,123 @@
// 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 from "react";
import { Dialog, DialogContent, DialogTitle } from "@material-ui/core";
import IconButton from "@material-ui/core/IconButton";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
interface IModalProps {
classes: any;
onClose: () => void;
modalOpen: boolean;
title: string;
children: any;
}
const baseCloseLine = {
content: '" "',
borderLeft: "2px solid #707070",
height: 33,
width: 1,
position: "absolute"
};
const styles = (theme: Theme) =>
createStyles({
dialogContainer: {
padding: "12px 26px 22px"
},
closeContainer: {
textAlign: "right"
},
closeButton: {
width: 45,
height: 45,
padding: 0,
backgroundColor: "initial",
"&:hover": {
backgroundColor: "initial"
},
"&:active": {
backgroundColor: "initial"
}
},
modalCloseIcon: {
fontSize: 35,
color: "#707070",
fontWeight: 300,
"&:hover": {
color: "#000"
}
},
closeIcon: {
"&::before": {
...baseCloseLine,
transform: "rotate(45deg)"
},
"&::after": {
...baseCloseLine,
transform: "rotate(-45deg)"
},
"&:hover::before, &:hover::after": {
borderColor: "#000"
},
width: 24,
height: 24,
display: "block",
position: "relative"
},
titleClass: {
padding: "0px 24px 12px"
}
});
const ModalWrapper = ({
onClose,
modalOpen,
title,
children,
classes
}: IModalProps) => {
return (
<Dialog
open={modalOpen}
onClose={onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
maxWidth={"md"}
fullWidth
>
<div className={classes.dialogContainer}>
<div className={classes.closeContainer}>
<IconButton
aria-label="close"
className={classes.closeButton}
onClick={onClose}
disableRipple
>
<span className={classes.closeIcon} />
</IconButton>
</div>
<DialogTitle id="alert-dialog-title" className={classes.titleClass}>
{title}
</DialogTitle>
<DialogContent>{children}</DialogContent>
</div>
</Dialog>
);
};
export default withStyles(styles)(ModalWrapper);

View File

@@ -0,0 +1,84 @@
// 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 from "react";
import isString from "lodash/isString";
import { IconButton } from "@material-ui/core";
import ViewIcon from "./TableActionIcons/ViewIcon";
import PencilIcon from "./TableActionIcons/PencilIcon";
import DeleteIcon from "./TableActionIcons/DeleteIcon";
import { Link } from "react-router-dom";
interface IActionButton {
type: string;
onClick?: (id: string) => any;
to?: string;
valueToSend: any;
selected: boolean;
sendOnlyId?: boolean;
idField: string;
}
const defineIcon = (type: string, selected: boolean) => {
switch (type) {
case "view":
return <ViewIcon active={selected} />;
case "edit":
return <PencilIcon active={selected} />;
case "delete":
return <DeleteIcon active={selected} />;
}
return null;
};
const TableActionButton = ({
type,
onClick,
valueToSend,
idField,
selected,
to,
sendOnlyId = false
}: IActionButton) => {
const valueClick = sendOnlyId ? valueToSend[idField] : valueToSend;
const buttonElement = (
<IconButton
aria-label={type}
onClick={
onClick
? () => {
onClick(valueClick);
}
: () => null
}
>
{defineIcon(type, selected)}
</IconButton>
);
if (onClick) {
return buttonElement;
}
if (isString(to)) {
return <Link to={`${to}/${valueClick}`}>{buttonElement}</Link>;
}
return null;
};
export default TableActionButton;

View File

@@ -0,0 +1,23 @@
import React from "react";
import { IIcon, selected, unSelected } from "./common";
const DeleteIcon = ({ active = false }: IIcon) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
>
<g transform="translate(-1225 -657)">
<path
fill={active ? selected : unSelected}
d="M0,8H0a8,8,0,0,0,8,8H8a8,8,0,0,0,8-8h0A8,8,0,0,0,8,0H8A8,8,0,0,0,0,8Zm10.007,3.489L8,9.482,5.993,11.489,4.511,10.007,6.518,8,4.511,5.993,5.993,4.511,8,6.518l2.007-2.007,1.482,1.482L9.482,8l2.007,2.007Z"
transform="translate(1225 657)"
/>
</g>
</svg>
);
};
export default DeleteIcon;

View File

@@ -0,0 +1,21 @@
import React from "react";
import { IIcon, selected, unSelected } from "./common";
const PencilIcon = ({ active = false }: IIcon) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 13.833 13.833"
>
<path
fill={active ? selected : unSelected}
d="M2.934,16H0V13.066L10.607,2.459a1,1,0,0,1,1.414,0l1.52,1.52a1,1,0,0,1,0,1.414Z"
transform="translate(0 -2.167)"
/>
</svg>
);
};
export default PencilIcon;

View File

@@ -0,0 +1,21 @@
import React from "react";
import { IIcon, selected, unSelected } from "./common";
const ViewIcon = ({ active = false }: IIcon) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 11.856"
>
<path
fill={active ? selected : unSelected}
d="M-54,8l1.764,2.614A7.52,7.52,0,0,0-46,13.928h0a7.52,7.52,0,0,0,6.234-3.314L-38,8l-1.764-2.614A7.52,7.52,0,0,0-46,2.072h0a7.52,7.52,0,0,0-6.234,3.314Zm10.286,0A2.285,2.285,0,0,1-46,10.286,2.285,2.285,0,0,1-48.286,8,2.285,2.285,0,0,1-46,5.714,2.285,2.285,0,0,1-43.714,8Zm1.3,0A3.59,3.59,0,0,1-46,11.59,3.59,3.59,0,0,1-49.59,8,3.59,3.59,0,0,1-46,4.41,3.59,3.59,0,0,1-42.41,8Z"
transform="translate(54 -2.072)"
/>
</svg>
);
};
export default ViewIcon;

View File

@@ -0,0 +1,6 @@
export interface IIcon {
active: boolean;
}
export const unSelected = "#adadad";
export const selected = "#201763";

View File

@@ -0,0 +1,322 @@
// 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 from "react";
import get from "lodash/get";
import isString from "lodash/isString";
import {
LinearProgress,
TablePagination,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Paper,
Grid,
Checkbox
} from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { TablePaginationActionsProps } from "@material-ui/core/TablePagination/TablePaginationActions";
import TableActionButton from "./TableActionButton";
//Interfaces for table Items
interface ItemActions {
type: string;
onClick?(valueToSend: any): any;
to?: string;
sendOnlyId?: boolean;
}
interface IColumns {
label: string;
elementKey: string;
sortable?: boolean;
renderFunction?: (input: any) => any;
}
interface IPaginatorConfig {
rowsPerPageOptions: number[];
colSpan: number;
count: number;
rowsPerPage: number;
page: number;
SelectProps: any;
onChangePage: (
event: React.MouseEvent<HTMLButtonElement> | null,
page: number
) => void;
onChangeRowsPerPage?: React.ChangeEventHandler<
HTMLTextAreaElement | HTMLInputElement
>;
ActionsComponent?: React.ElementType<TablePaginationActionsProps>;
}
interface TableWrapperProps {
itemActions?: ItemActions[] | null;
columns: IColumns[];
onSelect?: (e: React.ChangeEvent<HTMLInputElement>) => any;
idField: string;
isLoading: boolean;
records: any[];
classes: any;
entityName: string;
selectedItems?: string[];
stickyHeader?: boolean;
paginatorConfig?: IPaginatorConfig;
}
const borderColor = "#eaeaea";
const rowText = {
fontWeight: 400,
fontSize: 14,
borderColor: borderColor
};
const checkBoxBasic = {
width: 16,
height: 16,
borderRadius: 3
};
const styles = (theme: Theme) =>
createStyles({
dialogContainer: {
padding: "12px 26px 22px"
},
paper: {
display: "flex",
overflow: "auto",
flexDirection: "column",
padding: "19px 38px"
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: 700,
fontSize: 14,
paddingBottom: 15,
borderColor: borderColor
}
}
},
rowUnselected: {
...rowText
},
rowSelected: {
...rowText,
color: "#201763"
},
paginatorContainer: {
display: "flex",
justifyContent: "flex-end",
padding: "5px 38px"
},
checkBoxHeader: {
"&.MuiTableCell-paddingCheckbox": {
paddingBottom: 9
}
},
actionsContainer: {
width: 120,
borderColor: borderColor
},
paginatorComponent: {
borderBottom: 0
},
unCheckedIcon: { ...checkBoxBasic, border: "1px solid #d0d0d0" },
checkedIcon: {
...checkBoxBasic,
border: "1px solid #201763",
backgroundColor: "#201763"
},
checkBoxRow: {
borderColor: borderColor
}
});
// Function that renders Title Columns
const titleColumnsMap = (columns: IColumns[]) => {
return columns.map((column: IColumns, index: number) => {
return (
<TableCell key={`tbCT-${column.elementKey}-${index}`}>
{column.label}
</TableCell>
);
});
};
// Function that renders Rows
const rowColumnsMap = (
columns: IColumns[],
itemData: any,
classes: any,
isSelected: boolean
) => {
return columns.map((column: IColumns, index: number) => {
const itemElement = isString(itemData)
? itemData
: get(itemData, column.elementKey, null); // If the element is just a string, we render it as it is
const renderElement = column.renderFunction
? column.renderFunction(itemElement)
: itemElement; // If render function is set, we send the value to the function.
return (
<TableCell
key={`tbRE-${column.elementKey}-${index}`}
className={isSelected ? classes.rowSelected : classes.rowUnselected}
>
{renderElement}
</TableCell>
);
});
};
// Function to render the action buttons
const elementActions = (
actions: ItemActions[],
valueToSend: any,
selected: boolean,
idField: string
) => {
return actions.map((action: ItemActions, index: number) => {
return (
<TableActionButton
type={action.type}
onClick={action.onClick}
to={action.to}
valueToSend={valueToSend}
selected={selected}
key={`actions-${action.type}-${index.toString()}`}
idField={idField}
sendOnlyId={!!action.sendOnlyId}
/>
);
});
};
// Main function to render the Table Wrapper
const TableWrapper = ({
itemActions,
columns,
onSelect,
records,
isLoading,
entityName,
selectedItems,
idField,
classes,
stickyHeader = false,
paginatorConfig
}: TableWrapperProps) => {
return (
<Grid item xs={12}>
<Paper className={classes.paper}>
{isLoading && <LinearProgress />}
{records && records.length > 0 ? (
<Table size="small" stickyHeader={stickyHeader}>
<TableHead className={classes.minTableHeader}>
<TableRow>
{onSelect && selectedItems && (
<TableCell
padding="checkbox"
align="center"
className={classes.checkBoxHeader}
>
Select
</TableCell>
)}
{titleColumnsMap(columns)}
{itemActions && itemActions.length > 0 && (
<TableCell
align="center"
className={classes.actionsContainer}
>
Actions
</TableCell>
)}
</TableRow>
</TableHead>
<TableBody>
{records.map((record: any, index: number) => {
const isSelected = selectedItems
? selectedItems.includes(
isString(record) ? record : record[idField]
)
: false;
return (
<TableRow key={`tb-${entityName}-${index.toString()}`}>
{onSelect && selectedItems && (
<TableCell
padding="checkbox"
align="center"
className={classes.checkBoxRow}
>
<Checkbox
value={isString(record) ? record : record[idField]}
color="primary"
inputProps={{ "aria-label": "secondary checkbox" }}
checked={isSelected}
onChange={onSelect}
checkedIcon={<span className={classes.checkedIcon} />}
icon={<span className={classes.unCheckedIcon} />}
/>
</TableCell>
)}
{rowColumnsMap(columns, record, classes, isSelected)}
{itemActions && itemActions.length > 0 && (
<TableCell
align="center"
className={classes.actionsContainer}
>
{elementActions(
itemActions,
record,
isSelected,
idField
)}
</TableCell>
)}
</TableRow>
);
})}
</TableBody>
</Table>
) : (
<div>{`There are no ${entityName} yet.`}</div>
)}
</Paper>
{paginatorConfig && (
<Grid item xs={12} className={classes.paginatorContainer}>
<Table>
<TableBody>
<TableRow>
<TablePagination
{...paginatorConfig}
className={classes.paginatorComponent}
/>
</TableRow>
</TableBody>
</Table>
</Grid>
)}
</Grid>
);
};
export default withStyles(styles)(TableWrapper);

View File

@@ -0,0 +1,134 @@
// 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 { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Grid from "@material-ui/core/Grid";
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[];
classes: any;
}
const styles = (theme: Theme) => createStyles({});
const ConfTargetGeneric = ({
onChange,
fields,
classes
}: IConfGenericProps) => {
const [valueHolder, setValueHolder] = useState<IElementValue[]>([]);
const fieldsElements = !fields ? [] : fields;
// Effect to create all the values to hold
useEffect(() => {
const values: IElementValue[] = [];
fields.forEach(field => {
const stateInsert: IElementValue = {
key: field.name,
value: field.type === "on|off" ? "false" : ""
};
values.push(stateInsert);
});
setValueHolder(values);
}, [fields]);
useEffect(() => {
onChange(valueHolder);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [valueHolder]);
const setValueElement = (key: string, value: string, index: number) => {
const valuesDup = [...valueHolder];
valuesDup[index] = { key, value };
setValueHolder(valuesDup);
};
const fieldDefinition = (field: KVField, item: number) => {
switch (field.type) {
case "on|off":
return (
<RadioGroupSelector
currentSelection={valueHolder[item] ? valueHolder[item].value : ""}
id={field.name}
name={field.name}
label={field.label}
tooltip={field.tooltip}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setValueElement(field.name, e.target.value, item)
}
selectorOptions={[
{ label: "On", value: "true" },
{ label: "Off", value: "false" }
]}
/>
);
case "csv":
return (
<CSVMultiSelector
elements={valueHolder[item] ? valueHolder[item].value : ""}
label={field.label}
name={field.name}
onChange={(value: string) =>
setValueElement(field.name, value, item)
}
tooltip={field.tooltip}
/>
);
default:
return (
<InputBoxWrapper
id={field.name}
name={field.name}
label={field.label}
tooltip={field.tooltip}
value={valueHolder[item] ? valueHolder[item].value : ""}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setValueElement(field.name, e.target.value, item)
}
multiline={!!field.multiline}
/>
);
}
};
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>
</Grid>
);
};
export default withStyles(styles)(ConfTargetGeneric);

View File

@@ -0,0 +1,155 @@
// 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, { useState } from "react";
import get from "lodash/get";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import { TextField } from "@material-ui/core";
import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
import history from "../../../../history";
import TableWrapper from "../../Common/TableWrapper/TableWrapper";
import { configurationElements } from "../utils";
import { IConfigurationElement } from "../types";
import EditConfiguration from "../CustomForms/EditConfiguration";
interface IListConfiguration {
classes: any;
}
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
},
strongText: {
fontWeight: 700
},
keyName: {
marginLeft: 5
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10
}
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012"
},
iconText: {
lineHeight: "24px"
}
});
const ConfigurationsList = ({ classes }: IListConfiguration) => {
const [editScreenOpen, setEditScreenOpen] = useState(false);
const [selectedConfiguration, setSelectedConfiguration] = useState({
configuration_id: "",
configuration_label: ""
});
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState("");
const [filter, setFilter] = useState("");
const tableActions = [
{
type: "edit",
onClick: (element: IConfigurationElement) => {
const url = get(element, "url", "");
if (url !== "") {
// We redirect Browser
history.push(url);
} else {
setSelectedConfiguration(element);
setEditScreenOpen(true);
}
}
}
];
const filteredRecords: IConfigurationElement[] = configurationElements.filter(
elementItem =>
elementItem.configuration_id
.toLocaleLowerCase()
.includes(filter.toLocaleLowerCase())
);
return (
<React.Fragment>
{editScreenOpen && (
<EditConfiguration
open={editScreenOpen}
closeModalAndRefresh={() => {
setIsLoading(true);
setEditScreenOpen(false);
}}
selectedConfiguration={selectedConfiguration}
/>
)}
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">Configurations List</Typography>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
{error !== "" && <Grid container>{error}</Grid>}
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Filter"
className={classes.searchField}
id="search-resource"
label=""
onChange={event => {
setFilter(event.target.value);
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
}}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<TableWrapper
itemActions={tableActions}
columns={[
{ label: "Configuration", elementKey: "configuration_id" }
]}
isLoading={isLoading}
records={filteredRecords}
entityName="Configurations"
idField="configuration_id"
/>
</Grid>
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(ConfigurationsList);

View File

@@ -0,0 +1,188 @@
// 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, { useState } from "react";
import get from "lodash/get";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import { Button, TextField } from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
import AddIcon from "@material-ui/icons/Add";
import TableWrapper from "../../Common/TableWrapper/TableWrapper";
import EditConfiguration from "../CustomForms/EditConfiguration";
interface IMatchParams {
isExact: boolean;
params: any;
path: string;
}
interface IWebhookPanel {
match: IMatchParams;
classes: any;
}
interface IWebhook {
name: string;
}
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
},
strongText: {
fontWeight: 700
},
keyName: {
marginLeft: 5
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10
}
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012"
},
iconText: {
lineHeight: "24px"
}
});
const panels = {
logger: {
main: "logger",
title: "Logger Webhook Configuration",
modalTitle: "Logger Webhook",
apiURL: "",
configuration: {
configuration_id: "logger_webhook",
configuration_label: "Logger Webhook"
}
},
audit: {
main: "audit",
title: "Audit Webhook Configuration",
modalTitle: "Audit Webhook",
apiURL: "",
configuration: {
configuration_id: "audit_webhook",
configuration_label: "Audit Webhook"
}
}
};
const WebhookPanel = ({ match, classes }: IWebhookPanel) => {
const [addWebhookOpen, setAddWebhookOpen] = useState<boolean>(false);
const [error, setError] = useState<string>("");
const [filter, setFilter] = useState<string>("");
const [isLoading, setIsLoading] = useState<boolean>(false);
const [webhooks, setWebhooks] = useState<IWebhook[]>([]);
const pathIn = get(match, "path", "");
const panelToDisplay = pathIn.split("/");
const panelData = get(panels, panelToDisplay[2], false);
if (!panelData) {
return null;
}
const filteredRecords: IWebhook[] = webhooks.filter(elementItem =>
elementItem.name.toLocaleLowerCase().includes(filter.toLocaleLowerCase())
);
const tableActions = [
{
type: "edit",
onClick: () => {}
}
];
return (
<React.Fragment>
{addWebhookOpen && (
<EditConfiguration
open={addWebhookOpen}
closeModalAndRefresh={() => {
setIsLoading(true);
setAddWebhookOpen(false);
}}
selectedConfiguration={panelData.configuration}
/>
)}
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">{panelData.title}</Typography>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
{error !== "" && <Grid container>{error}</Grid>}
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Filter"
className={classes.searchField}
id="search-resource"
label=""
onChange={event => {
setFilter(event.target.value);
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<AddIcon />}
onClick={() => {
setAddWebhookOpen(true);
}}
>
Add Webhook Configuration
</Button>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<TableWrapper
itemActions={tableActions}
columns={[{ label: "Name", elementKey: "name" }]}
isLoading={isLoading}
records={filteredRecords}
entityName="Webhook Configurations"
idField="name"
/>
</Grid>
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(WebhookPanel);

View File

@@ -0,0 +1,292 @@
// 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, { useCallback, useEffect, useState } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { FormControlLabel, Switch } from "@material-ui/core";
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";
interface IConfMySqlProps {
onChange: (newValue: IElementValue[]) => void;
classes: any;
}
const styles = (theme: Theme) => createStyles({});
const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
//Local States
const [useDsnString, setUseDsnString] = useState<boolean>(false);
const [dsnString, setDsnString] = useState<string>("");
const [host, setHostname] = useState<string>("");
const [dbName, setDbName] = useState<string>("");
const [port, setPort] = useState<string>("");
const [user, setUser] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [table, setTable] = useState<string>("");
const [format, setFormat] = useState<string>("namespace");
const [queueDir, setQueueDir] = useState<string>("");
const [queueLimit, setQueueLimit] = useState<string>("");
const [comment, setComment] = useState<string>("");
// dsn_string* (string) MySQL data-source-name connection string e.g. "<user>:<password>@tcp(<host>:<port>)/<database>"
// table* (string) DB table name to store/update events, table is auto-created
// format* (namespace*|access) 'namespace' reflects current bucket/object list and 'access' reflects a journal of object operations, defaults to 'namespace'
// queue_dir (path) staging dir for undelivered messages e.g. '/home/events'
// queue_limit (number) maximum limit for undelivered messages, defaults to '100000'
// comment (sentence) optionally add a comment to this setting
const parseDsnString = (
input: string,
keys: string[]
): Map<string, string> => {
let kvFields: Map<string, string> = new Map();
const regex = /(.*?):(.*?)@tcp\((.*?):(.*?)\)\/(.*?)$/gm;
let m;
while ((m = regex.exec(input)) !== null) {
// This is necessary to avoid infinite loops with zero-width matches
if (m.index === regex.lastIndex) {
regex.lastIndex++;
}
kvFields.set("user", m[1]);
kvFields.set("password", m[2]);
kvFields.set("host", m[3]);
kvFields.set("port", m[4]);
kvFields.set("dbname", m[5]);
}
return kvFields;
};
const configToDsnString = useCallback((): string => {
return `${user}:${password}@tcp(${host}:${port})/${dbName}`;
}, [user, password, host, port, dbName]);
useEffect(() => {
if (dsnString !== "") {
const formValues = [
{ key: "dsn_string", value: dsnString },
{ key: "table", value: table },
{ key: "format", value: format },
{ key: "queue_dir", value: queueDir },
{ key: "queue_limit", value: queueLimit },
{ key: "comment", value: comment }
];
onChange(formValues);
}
}, [dsnString, table, format, queueDir, queueLimit, comment, onChange]);
useEffect(() => {
const cs = configToDsnString();
setDsnString(cs);
}, [user, dbName, password, port, host, setDsnString, configToDsnString]);
return (
<Grid container>
<Grid item xs={12}>
<FormControlLabel
control={
<Switch
checked={useDsnString}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.checked) {
// build dsn_string
const cs = configToDsnString();
setDsnString(cs);
} else {
// parse dsn_string
const kv = parseDsnString(dsnString, [
"host",
"port",
"dbname",
"user",
"password"
]);
setHostname(kv.get("host") ? kv.get("host") + "" : "");
setPort(kv.get("port") ? kv.get("port") + "" : "");
setDbName(kv.get("dbname") ? kv.get("dbname") + "" : "");
setUser(kv.get("user") ? kv.get("user") + "" : "");
setPassword(
kv.get("password") ? kv.get("password") + "" : ""
);
}
setUseDsnString(event.target.checked);
}}
name="checkedB"
color="primary"
/>
}
label="Enter DSN String"
/>
</Grid>
{useDsnString ? (
<React.Fragment>
<Grid item xs={12}>
<InputBoxWrapper
id="dsn-string"
name="dsn_string"
label="DSN String"
value={dsnString}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setDsnString(e.target.value);
}}
/>
</Grid>
</React.Fragment>
) : (
<React.Fragment>
<Grid item xs={12}>
<InputBoxWrapper
id="host"
name="host"
label="Host"
value={host}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setHostname(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="db-name"
name="db-name"
label="DB Name"
value={dbName}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setDbName(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="port"
name="port"
label="Port"
value={port}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setPort(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="user"
name="user"
label="User"
value={user}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setUser(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="password"
name="password"
label="Password"
type="password"
value={password}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setPassword(e.target.value);
}}
/>
</Grid>
</React.Fragment>
)}
<Grid item xs={12}>
<InputBoxWrapper
id="table"
name="table"
label="Table"
value={table}
tooltip="DB table name to store/update events, table is auto-created"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setTable(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<RadioGroupSelector
currentSelection={format}
id="format"
name="format"
label="Format"
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" }
]}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="queue-dir"
name="queue_dir"
label="Queue Dir"
value={queueDir}
tooltip="staging dir for undelivered messages e.g. '/home/events'"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setQueueDir(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="queue-limit"
name="queue_limit"
label="Queue Limit"
type="number"
value={queueLimit}
tooltip="maximum limit for undelivered messages, defaults to '10000'"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setQueueLimit(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="comment"
name="comment"
label="Comment"
multiline={true}
value={comment}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setComment(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<br />
</Grid>
</Grid>
);
};
export default withStyles(styles)(ConfMySql);

View File

@@ -0,0 +1,380 @@
// 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, { useCallback, useEffect, useState } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { FormControlLabel, Switch } from "@material-ui/core";
import Grid from "@material-ui/core/Grid";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import RadioGroupSelector from "../../Common/FormComponents/RadioGroupSelector/RadioGroupSelector";
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
import { IElementValue } from "../types";
interface IConfPostgresProps {
onChange: (newValue: IElementValue[]) => void;
classes: any;
}
const styles = (theme: Theme) => createStyles({});
const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
//Local States
const [useConnectionString, setUseConnectionString] = useState<boolean>(
false
);
const [connectionString, setConnectionString] = useState<string>("");
const [host, setHostname] = useState<string>("");
const [dbName, setDbName] = useState<string>("");
const [port, setPort] = useState<string>("");
const [user, setUser] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [sslMode, setSslMode] = useState<string>("require");
const [table, setTable] = useState<string>("");
const [format, setFormat] = useState<string>("namespace");
const [queueDir, setQueueDir] = useState<string>("");
const [queueLimit, setQueueLimit] = useState<string>("");
const [comment, setComment] = useState<string>("");
// connection_string* (string) Postgres server connection-string e.g. "host=localhost port=5432 dbname=minio_events user=postgres password=password sslmode=disable"
// "host=localhost
// port=5432
//dbname=minio_events
//user=postgres
//password=password
//sslmode=disable"
// table* (string) DB table name to store/update events, table is auto-created
// format* (namespace*|access) 'namespace' reflects current bucket/object list and 'access' reflects a journal of object operations, defaults to 'namespace'
// queue_dir (path) staging dir for undelivered messages e.g. '/home/events'
// queue_limit (number) maximum limit for undelivered messages, defaults to '10000'
// comment (sentence) optionally add a comment to this setting
const KvSeparator = "=";
const parseConnectionString = (
input: string,
keys: string[]
): Map<string, string> => {
let valueIndexes: number[] = [];
for (const key of keys) {
const i = input.indexOf(key + KvSeparator);
if (i === -1) {
continue;
}
valueIndexes.push(i);
}
valueIndexes.sort((n1, n2) => n1 - n2);
let kvFields = new Map<string, string>();
let fields: string[] = new Array<string>(valueIndexes.length);
for (let i = 0; i < valueIndexes.length; i++) {
const j = i + 1;
if (j < valueIndexes.length) {
fields[i] = input.substr(
valueIndexes[i],
valueIndexes[j] - valueIndexes[i]
);
} else {
fields[i] = input.substr(valueIndexes[i]);
}
}
for (let field of fields) {
if (field === undefined) {
continue;
}
const key = field.substr(0, field.indexOf("="));
const value = field.substr(field.indexOf("=") + 1).trim();
kvFields.set(key, value);
}
return kvFields;
};
const configToString = useCallback((): string => {
let strValue = "";
if (host !== "") {
strValue = `${strValue} host=${host}`;
}
if (dbName !== "") {
strValue = `${strValue} dbname=${dbName}`;
}
if (user !== "") {
strValue = `${strValue} user=${user}`;
}
if (password !== "") {
strValue = `${strValue} password=${password}`;
}
if (port !== "") {
strValue = `${strValue} port=${port}`;
}
strValue = `${strValue} sslmode=${sslMode}`;
return strValue.trim();
}, [host, dbName, user, password, port, sslMode]);
useEffect(() => {
if (connectionString !== "") {
const formValues = [
{ key: "connection_string", value: connectionString },
{ key: "table", value: table },
{ key: "format", value: format },
{ key: "queue_dir", value: queueDir },
{ key: "queue_limit", value: queueLimit },
{ key: "comment", value: comment }
];
onChange(formValues);
}
}, [
connectionString,
table,
format,
queueDir,
queueLimit,
comment,
onChange
]);
useEffect(() => {
const cs = configToString();
setConnectionString(cs);
}, [
user,
dbName,
password,
port,
sslMode,
host,
setConnectionString,
configToString
]);
return (
<Grid container>
<Grid item xs={12}>
<FormControlLabel
control={
<Switch
checked={useConnectionString}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.checked) {
// build connection_string
const cs = configToString();
setConnectionString(cs);
} else {
// parse connection_string
const kv = parseConnectionString(connectionString, [
"host",
"port",
"dbname",
"user",
"password",
"sslmode"
]);
setHostname(kv.get("host") ? kv.get("host") + "" : "");
setPort(kv.get("port") ? kv.get("port") + "" : "");
setDbName(kv.get("dbname") ? kv.get("dbname") + "" : "");
setUser(kv.get("user") ? kv.get("user") + "" : "");
setPassword(
kv.get("password") ? kv.get("password") + "" : ""
);
setSslMode(
kv.get("sslmode") ? kv.get("sslmode") + "" : "require"
);
}
setUseConnectionString(event.target.checked);
}}
name="checkedB"
color="primary"
/>
}
label="Enter Connection String"
/>
</Grid>
{useConnectionString ? (
<React.Fragment>
<Grid item xs={12}>
<InputBoxWrapper
id="connection-string"
name="connection_string"
label="Connection String"
value={connectionString}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setConnectionString(e.target.value);
}}
/>
</Grid>
</React.Fragment>
) : (
<React.Fragment>
<Grid item xs={12}>
<InputBoxWrapper
id="host"
name="host"
label="Host"
value={host}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setHostname(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="db-name"
name="db-name"
label="DB Name"
value={dbName}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setDbName(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="port"
name="port"
label="Port"
value={port}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setPort(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<SelectWrapper
value={sslMode}
label="SSL Mode"
id="sslmode"
name="sslmode"
onChange={(e): void => {
if (e.target.value !== undefined) {
setSslMode(e.target.value + "");
}
}}
options={[
{ label: "Require", value: "require" },
{ label: "Disable", value: "disable" },
{ label: "Verify CA", value: "verify-ca" },
{ label: "Verify Full", value: "verify-full" }
]}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="user"
name="user"
label="User"
value={user}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setUser(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="password"
name="password"
label="Password"
type="password"
value={password}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setPassword(e.target.value);
}}
/>
</Grid>
</React.Fragment>
)}
<Grid item xs={12}>
<InputBoxWrapper
id="table"
name="table"
label="Table"
value={table}
tooltip="DB table name to store/update events, table is auto-created"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setTable(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<RadioGroupSelector
currentSelection={format}
id="format"
name="format"
label="Format"
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" }
]}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="queue-dir"
name="queue_dir"
label="Queue Dir"
value={queueDir}
tooltip="staging dir for undelivered messages e.g. '/home/events'"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setQueueDir(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="queue-limit"
name="queue_limit"
label="Queue Limit"
type="number"
value={queueLimit}
tooltip="maximum limit for undelivered messages, defaults to '10000'"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setQueueLimit(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="comment"
name="comment"
label="Comment"
multiline={true}
value={comment}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setComment(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<br />
</Grid>
</Grid>
);
};
export default withStyles(styles)(ConfPostgres);

View File

@@ -0,0 +1,160 @@
// This file is part of MinIO Console Server
// Copyright (c) 2019 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, { useCallback, useEffect, useState } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Button } 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 { fieldsConfigurations, removeEmptyFields } from "../utils";
import { IConfigurationElement, IElementValue } from "../types";
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
},
strongText: {
fontWeight: 700
},
keyName: {
marginLeft: 5
},
buttonContainer: {
textAlign: "right"
},
logoButton: {
height: "80px"
}
});
interface IAddNotificationEndpointProps {
open: boolean;
closeModalAndRefresh: any;
serverNeedsRestart: typeof serverNeedsRestart;
selectedConfiguration: IConfigurationElement;
classes: any;
}
const EditConfiguration = ({
open,
closeModalAndRefresh,
serverNeedsRestart,
selectedConfiguration,
classes
}: IAddNotificationEndpointProps) => {
//Local States
const [valuesObj, setValueObj] = useState<IElementValue[]>([]);
const [saving, setSaving] = useState<boolean>(false);
const [addError, setError] = useState<string>("");
//Effects
useEffect(() => {
if (saving) {
const payload = {
key_values: removeEmptyFields(valuesObj)
};
api
.invoke(
"PUT",
`/api/v1/configs/${selectedConfiguration.configuration_id}`,
payload
)
.then(res => {
setSaving(false);
setError("");
serverNeedsRestart(true);
closeModalAndRefresh();
})
.catch(err => {
setSaving(false);
setError(err);
});
}
}, [
saving,
serverNeedsRestart,
selectedConfiguration,
valuesObj,
closeModalAndRefresh
]);
//Fetch Actions
const submitForm = (event: React.FormEvent) => {
event.preventDefault();
setSaving(true);
};
const onValueChange = useCallback(
newValue => {
setValueObj(newValue);
},
[setValueObj]
);
return (
<ModalWrapper
modalOpen={open}
onClose={closeModalAndRefresh}
title={selectedConfiguration.configuration_label}
>
<React.Fragment>
{addError !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
</Grid>
)}
<form noValidate onSubmit={submitForm}>
<ConfTargetGeneric
fields={
fieldsConfigurations[selectedConfiguration.configuration_id]
}
onChange={onValueChange}
/>
<Grid item xs={3} className={classes.buttonContainer}>
<Button
type="submit"
variant="contained"
color="primary"
fullWidth
disabled={saving}
>
Save
</Button>
</Grid>
<Grid item xs={9} />
</form>
</React.Fragment>
</ModalWrapper>
);
};
const connector = connect(null, { serverNeedsRestart });
export default connector(withStyles(styles)(EditConfiguration));

View File

@@ -0,0 +1,51 @@
// 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 { SelectorTypes } from "../Common/FormComponents/RadioGroupSelector/RadioGroupSelector";
export type KVFieldType =
| "string"
| "number"
| "on|off"
| "enum"
| "path"
| "url"
| "address"
| "duration"
| "uri"
| "sentence"
| "csv";
export interface KVField {
name: string;
label: string;
tooltip: string;
required?: boolean;
type: KVFieldType;
options?: SelectorTypes[];
multiline?: boolean;
}
export interface IConfigurationElement {
configuration_id: string;
configuration_label: string;
url?: string;
}
export interface IElementValue {
key: string;
value: string;
}

View File

@@ -0,0 +1,771 @@
// 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 { IConfigurationElement, IElementValue } from "./types";
export const notifyPostgres = "notify_postgres";
export const notifyMysql = "notify_mysql";
export const notifyKafka = "notify_kafka";
export const notifyAmqp = "notify_amqp";
export const notifyMqtt = "notify_mqtt";
export const notifyRedis = "notify_redis";
export const notifyNats = "notify_nats";
export const notifyElasticsearch = "notify_elasticsearch";
export const notifyWebhooks = "notify_webhooks";
export const notifyNsq = "notify_nsq";
export const configurationElements: IConfigurationElement[] = [
{ configuration_id: "region", configuration_label: "Region Configuration" },
{ configuration_id: "cache", configuration_label: "Cache Configuration" },
{
configuration_id: "compression",
configuration_label: "Compression Configuration"
},
{ configuration_id: "etcd", configuration_label: "Etcd Configuration" },
{
configuration_id: "identity_openid",
configuration_label: "Identity Openid Configuration"
},
{
configuration_id: "identity_ldap",
configuration_label: "Identity LDAP Configuration"
},
{
configuration_id: "policy_opa",
configuration_label: "Policy OPA Configuration"
},
{
configuration_id: "kms_vault",
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 = {
region: [
{
name: "name",
required: true,
label: "name",
tooltip: 'Name of the location of the server e.g. "us-west-rack2"',
type: "string"
},
{
name: "comment",
required: false,
label: "comment",
tooltip: "You can add a comment to this setting",
type: "string",
multiline: true
}
],
cache: [
{
name: "drives",
required: true,
label: "Drives",
tooltip:
'Mountpoints e.g. "/optane1" or "/optane2", you can write one per field',
type: "csv"
},
{
name: "expiry",
required: false,
label: "Expiry",
tooltip: 'Cache expiry duration in days e.g. "90"',
type: "number"
},
{
name: "quota",
required: false,
label: "Quota",
tooltip: 'Limit cache drive usage in percentage e.g. "90"',
type: "number"
},
{
name: "exclude",
required: false,
label: "Exclude",
tooltip:
'Wildcard exclusion patterns e.g. "bucket/*.tmp" or "*.exe", you can write one per field',
type: "csv"
},
{
name: "after",
required: false,
label: "After",
tooltip: "Minimum number of access before caching an object",
type: "number"
},
{
name: "comment",
required: false,
label: "Comment",
tooltip: "You can add a comment to this setting",
type: "string",
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"
},
{
name: "mime_types",
required: false,
label: "Mime Types",
tooltip:
'Mime types e.g. "text/*","application/json" or "application/xml", you can write one per field',
type: "csv"
}
],
etcd: [
{
name: "endpoints",
required: true,
label: "Endpoints",
tooltip:
'List of etcd endpoints e.g. "http://localhost:2379", you can write one per field',
type: "csv"
},
{
name: "path_prefix",
required: false,
label: "Path Prefix",
tooltip: 'namespace prefix to isolate tenants e.g. "customer1/"',
type: "string"
},
{
name: "coredns_path",
required: false,
label: "Coredns Path",
tooltip: 'Shared bucket DNS records, default is "/skydns"',
type: "string"
},
{
name: "client_cert",
required: false,
label: "Client Cert",
tooltip: "Client cert for mTLS authentication",
type: "string"
},
{
name: "client_cert_key",
required: false,
label: "Client Cert Key",
tooltip: "Client cert key for mTLS authentication",
type: "string"
},
{
name: "comment",
required: false,
label: "Comment",
tooltip: "You can add a comment to this setting",
type: "string",
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"
}
],
identity_ldap: [
{
name: "server_addr",
required: true,
label: "Server ADDR",
tooltip: 'AD/LDAP server address e.g. "myldapserver.com:636"',
type: "string"
},
{
name: "username_format",
required: true,
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"
},
{
name: "username_search_filter",
required: true,
label: "Username Search Filter",
tooltip:
'User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)"',
type: "string"
},
{
name: "group_search_filter",
required: true,
label: "Group Search Filter",
tooltip:
'Search filter for groups e.g. "(&(objectclass=groupOfNames)(memberUid=%s))"',
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"
},
{
name: "group_name_attribute",
required: false,
label: "Group Name Attribute",
tooltip: 'Search attribute for group name e.g. "cn"',
type: "string"
},
{
name: "sts_expiry",
required: false,
label: "STS Expiry",
tooltip:
'temporary credentials validity duration in s,m,h,d. Default is "1h"',
type: "string"
},
{
name: "tls_skip_verify",
required: false,
label: "TLS Skip Verify",
tooltip:
'Trust server TLS without verification, defaults to "off" (verify)',
type: "on|off"
},
{
name: "server_insecure",
required: false,
label: "Server Insecure",
tooltip:
'Allow plain text connection to AD/LDAP server, defaults to "off"',
type: "on|off"
},
{
name: "comment",
required: false,
label: "Comment",
tooltip: "Optionally add a comment to this setting",
type: "string",
multiline: true
}
],
policy_opa: [
{
name: "opa_url",
required: true,
label: "OPA URL",
type: "string"
}
],
kms_vault: [],
kms_kes: [],
logger_webhook: [
{
name: "name",
required: true,
label: "Name",
tooltip: "Name of the webhook",
type: "string"
},
{
name: "auth_token",
required: true,
label: "Auth Token",
type: "string"
},
{
name: "endpoint",
required: true,
label: "Endpoint",
type: "string"
}
],
audit_webhook: [
{
name: "name",
required: true,
label: "Name",
tooltip: "Name of the webhook",
type: "string"
},
{
name: "auth_token",
required: true,
label: "Auth Token",
type: "string"
},
{
name: "endpoint",
required: true,
label: "Endpoint",
type: "string"
}
]
};
const commonFields = [
{
name: "queue-dir",
label: "Queue Dir",
required: true,
tooltip: "staging dir for undelivered messages e.g. '/home/events'",
type: "string"
},
{
name: "queue-limit",
label: "Queue Limit",
required: false,
tooltip: "maximum limit for undelivered messages, defaults to '10000'",
type: "number"
},
{
name: "comment",
label: "Comment",
required: false,
type: "string",
multiline: true
}
];
export const notificationEndpointsFields: any = {
[notifyKafka]: [
{
name: "brokers",
label: "Brokers",
required: true,
tooltip: "comma separated list of Kafka broker addresses",
type: "string"
},
{
name: "topic",
label: "Topic",
tooltip: "Kafka topic used for bucket notifications",
type: "string"
},
{
name: "sasl_username",
label: "SASL Username",
tooltip: "username for SASL/PLAIN or SASL/SCRAM authentication",
type: "string"
},
{
name: "sasl_password",
label: "SASL Password",
tooltip: "password for SASL/PLAIN or SASL/SCRAM authentication",
type: "string"
},
{
name: "sasl_mechanism",
label: "SASL Mechanism",
tooltip: "sasl authentication mechanism, default 'PLAIN'",
type: "string"
},
{
name: "tls_client_auth",
label: "TLS Client Auth",
tooltip:
"clientAuth determines the Kafka server's policy for TLS client auth",
type: "string"
},
{
name: "sasl",
label: "SASL",
tooltip: "set to 'on' to enable SASL authentication",
type: "on|off"
},
{
name: "tls",
label: "TLS",
tooltip: "set to 'on' to enable TLS",
type: "on|off"
},
{
name: "tls_skip_verify",
label: "TLS skip verify",
tooltip:
'trust server TLS without verification, defaults to "on" (verify)',
type: "on|off"
},
{
name: "client_tls_cert",
label: "client TLS cert",
tooltip: "path to client certificate for mTLS auth",
type: "path"
},
{
name: "client_tls_key",
label: "client TLS key",
tooltip: "path to client key for mTLS auth",
type: "path"
},
{
name: "version",
label: "Version",
tooltip: "specify the version of the Kafka cluster e.g '2.2.0'",
type: "string"
},
...commonFields
],
[notifyAmqp]: [
{
name: "url",
required: true,
label: "url",
tooltip:
"AMQP server endpoint e.g. `amqp://myuser:mypassword@localhost:5672`",
type: "url"
},
{
name: "exchange",
label: "exchange",
tooltip: "name of the AMQP exchange",
type: "string"
},
{
name: "exchange_type",
label: "exchange_type",
tooltip: "AMQP exchange type",
type: "string"
},
{
name: "routing_key",
label: "routing_key",
tooltip: "routing key for publishing",
type: "string"
},
{
name: "mandatory",
label: "mandatory",
tooltip:
"quietly ignore undelivered messages when set to 'off', default is 'on'",
type: "on|off"
},
{
name: "durable",
label: "durable",
tooltip:
"persist queue across broker restarts when set to 'on', default is '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"
},
{
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"
},
{
name: "auto_deleted",
label: "auto_deleted",
tooltip:
"auto delete queue when set to 'on', when there are no consumers",
type: "on|off"
},
{
name: "delivery_mode",
label: "delivery_mode",
tooltip: "set to '1' for non-persistent or '2' for persistent queue",
type: "number"
},
...commonFields
],
[notifyRedis]: [
{
name: "address",
required: true,
label: "address",
tooltip: "Redis server's address. For example: `localhost:6379`",
type: "address"
},
{
name: "key",
required: true,
label: "key",
tooltip: "Redis key to store/update events, key is auto-created",
type: "string"
},
{
name: "password",
label: "password",
tooltip: "Redis server password",
type: "string"
},
...commonFields
],
[notifyMqtt]: [
{
name: "broker",
required: true,
label: "broker",
tooltip: "MQTT server endpoint e.g. `tcp://localhost:1883`",
type: "uri"
},
{
name: "topic",
required: true,
label: "topic",
tooltip: "name of the MQTT topic to publish",
type: "string"
},
{
name: "username",
label: "username",
tooltip: "MQTT username",
type: "string"
},
{
name: "password",
label: "password",
tooltip: "MQTT password",
type: "string"
},
{
name: "qos",
label: "qos",
tooltip: "set the quality of service priority, defaults to '0'",
type: "number"
},
{
name: "keep_alive_interval",
label: "keep_alive_interval",
tooltip: "keep-alive interval for MQTT connections in s,m,h,d",
type: "duration"
},
{
name: "reconnect_interval",
label: "reconnect_interval",
tooltip: "reconnect interval for MQTT connections in s,m,h,d",
type: "duration"
},
...commonFields
],
[notifyNats]: [
{
name: "address",
required: true,
label: "address",
tooltip: "NATS server address e.g. '0.0.0.0:4222'",
type: "address"
},
{
name: "subject",
required: true,
label: "subject",
tooltip: "NATS subscription subject",
type: "string"
},
{
name: "username",
label: "username",
tooltip: "NATS username",
type: "string"
},
{
name: "password",
label: "password",
tooltip: "NATS password",
type: "string"
},
{
name: "token",
label: "token",
tooltip: "NATS token",
type: "string"
},
{
name: "tls",
label: "tls",
tooltip: "set to 'on' to enable TLS",
type: "on|off"
},
{
name: "tls_skip_verify",
label: "tls_skip_verify",
tooltip:
'trust server TLS without verification, defaults to "on" (verify)',
type: "on|off"
},
{
name: "ping_interval",
label: "ping_interval",
tooltip: "client ping commands interval in s,m,h,d. Disabled by default",
type: "duration"
},
{
name: "streaming",
label: "streaming",
tooltip: "set to 'on', to use streaming NATS server",
type: "on|off"
},
{
name: "streaming_async",
label: "streaming_async",
tooltip: "set to 'on', to enable asynchronous publish",
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"
},
{
name: "streaming_cluster_id",
label: "streaming_cluster_id",
tooltip: "unique ID for NATS streaming cluster",
type: "string"
},
{
name: "cert_authority",
label: "cert_authority",
tooltip: "path to certificate chain of the target NATS server",
type: "string"
},
{
name: "client_cert",
label: "client_cert",
tooltip: "client cert for NATS mTLS auth",
type: "string"
},
{
name: "client_key",
label: "client_key",
tooltip: "client cert key for NATS mTLS auth",
type: "string"
},
...commonFields
],
[notifyElasticsearch]: [
{
name: "url",
required: true,
label: "url",
tooltip:
"Elasticsearch server's address, with optional authentication info",
type: "url"
},
{
name: "index",
required: true,
label: "index",
tooltip:
"Elasticsearch index to store/update events, index is auto-created",
type: "string"
},
{
name: "format",
required: true,
label: "format",
tooltip:
"'namespace' reflects current bucket/object list and 'access' reflects a journal of object operations, defaults to 'namespace'",
type: "enum"
},
...commonFields
],
[notifyWebhooks]: [
{
name: "endpoint",
required: true,
label: "endpoint",
tooltip:
"webhook server endpoint e.g. http://localhost:8080/minio/events",
type: "url"
},
{
name: "auth_token",
label: "auth_token",
tooltip: "opaque string or JWT authorization token",
type: "string"
},
...commonFields
],
[notifyNsq]: [
{
name: "nsqd_address",
required: true,
label: "nsqd_address",
tooltip: "NSQ server address e.g. '127.0.0.1:4150'",
type: "address"
},
{
name: "topic",
required: true,
label: "topic",
tooltip: "NSQ topic",
type: "string"
},
{
name: "tls",
label: "tls",
tooltip: "set to 'on' to enable TLS",
type: "on|off"
},
{
name: "tls_skip_verify",
label: "tls_skip_verify",
tooltip:
'trust server TLS without verification, defaults to "on" (verify)',
type: "on|off"
},
...commonFields
]
};
export const removeEmptyFields = (formFields: IElementValue[]) => {
const nonEmptyFields = formFields.filter(field => field.value !== "");
return nonEmptyFields;
};

View File

@@ -40,7 +40,11 @@ import {
} from "react-router-dom";
import { connect } from "react-redux";
import { AppState } from "../../store";
import { setMenuOpen } from "../../actions";
import {
serverIsLoading,
serverNeedsRestart,
setMenuOpen
} from "../../actions";
import { ThemedComponentProps } from "@material-ui/core/styles/withTheme";
import Buckets from "./Buckets/Buckets";
import Policies from "./Policies/Policies";
@@ -53,6 +57,11 @@ import NotFoundPage from "../NotFoundPage";
import ServiceAccounts from "./ServiceAccounts/ServiceAccounts";
import Users from "./Users/Users";
import Groups from "./Groups/Groups";
import ListNotificationEndpoints from "./NotificationEndopoints/ListNotificationEndpoints";
import ConfigurationsList from "./Configurations/ConfigurationPanels/ConfigurationsList";
import { Button, LinearProgress } from "@material-ui/core";
import WebhookPanel from "./Configurations/ConfigurationPanels/WebhookPanel";
import Trace from "./Trace/Trace";
function Copyright() {
return (
@@ -150,27 +159,44 @@ const styles = (theme: Theme) =>
},
fixedHeight: {
minHeight: 240
},
warningBar: {
background: theme.palette.primary.main,
color: "white",
heigh: "60px",
widht: "100%",
lineHeight: "60px",
textAlign: "center"
}
});
const mapState = (state: AppState) => ({
open: state.system.sidebarOpen
open: state.system.sidebarOpen,
needsRestart: state.system.serverNeedsRestart,
isServerLoading: state.system.serverIsLoading
});
const connector = connect(mapState, { setMenuOpen });
const connector = connect(mapState, {
setMenuOpen,
serverNeedsRestart,
serverIsLoading
});
interface ConsoleProps {
interface IConsoleProps {
open: boolean;
needsRestart: boolean;
isServerLoading: boolean;
title: string;
classes: any;
setMenuOpen: typeof setMenuOpen;
serverNeedsRestart: typeof serverNeedsRestart;
serverIsLoading: typeof serverIsLoading;
}
class Console extends React.Component<
ConsoleProps & RouteComponentProps & StyledProps & ThemedComponentProps
IConsoleProps & RouteComponentProps & StyledProps & ThemedComponentProps
> {
componentDidMount(): void {
//TODO: verify the session is still valid
api
.invoke("GET", `/api/v1/session`)
.then(res => {
@@ -182,8 +208,25 @@ class Console extends React.Component<
});
}
restartServer() {
this.props.serverIsLoading(true);
api
.invoke("POST", "/api/v1/service/restart", {})
.then(res => {
console.log("success restarting service");
console.log(res);
this.props.serverIsLoading(false);
this.props.serverNeedsRestart(false);
})
.catch(err => {
this.props.serverIsLoading(false);
console.log("failure restarting service");
console.log(err);
});
}
render() {
const { classes, open } = this.props;
const { classes, open, needsRestart, isServerLoading } = this.props;
return (
<div className={classes.root}>
<CssBaseline />
@@ -209,6 +252,30 @@ class Console extends React.Component<
</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}>
@@ -224,6 +291,19 @@ class Console extends React.Component<
<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>

View File

@@ -25,6 +25,7 @@ import PieChartIcon from "@material-ui/icons/PieChart";
import ViewHeadlineIcon from "@material-ui/icons/ViewHeadline";
import { Usage } from "./types";
import api from "../../../common/api";
import { niceBytes } from "../../../common/utils";
const styles = (theme: Theme) =>
createStyles({
@@ -137,26 +138,15 @@ const Dashboard = ({ classes }: IDashboardProps) => {
});
};
const prettyUsage = (usage: string | undefined) => {
if (usage == undefined) {
if (usage === undefined) {
return "0";
}
return niceBytes(usage);
};
const units = ["bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const niceBytes = (x: string) => {
let l = 0,
n = parseInt(x, 10) || 0;
while (n >= 1024 && ++l) {
n = n / 1024;
}
//include a decimal point and a tenths-place digit if presenting
//less than ten of KB or greater units
return n.toFixed(n < 10 && l > 0 ? 1 : 0) + " " + units[l];
};
const prettyNumber = (usage: number | undefined) => {
if (usage == undefined) {
if (usage === undefined) {
return 0;
}
@@ -165,11 +155,12 @@ const Dashboard = ({ classes }: IDashboardProps) => {
return (
<React.Fragment>
<Grid container xs={12}>
<Grid container xs={12} spacing={3} className={classes.container}>
<Grid container xs={12}>
<Grid container>
<Grid container spacing={3} className={classes.container}>
<Grid container>
<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">

View File

@@ -14,26 +14,16 @@
// 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 React, { useEffect, useState } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import {
Button,
Dialog,
DialogContent,
DialogTitle,
LinearProgress,
TextField
} from "@material-ui/core";
import Radio from "@material-ui/core/Radio";
import RadioGroup from "@material-ui/core/RadioGroup";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import { Button, LinearProgress } from "@material-ui/core";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import api from "../../../common/api";
import UsersSelectors from "./UsersSelectors";
import { GroupsList } from "./types";
import { groupsSort } from "../../../utils/sortFunctions";
import Title from "../../../common/Title";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import RadioGroupSelector from "../Common/FormComponents/RadioGroupSelector/RadioGroupSelector";
interface IGroupProps {
open: boolean;
@@ -58,6 +48,9 @@ const styles = (theme: Theme) =>
},
keyName: {
marginLeft: 5
},
buttonContainer: {
textAlign: "right"
}
});
@@ -87,15 +80,69 @@ const AddGroup = ({
useEffect(() => {
if (saving) {
const saveRecord = () => {
if (selectedGroup !== null) {
api
.invoke("PUT", `/api/v1/groups/${groupName}`, {
group: groupName,
members: selectedUsers,
status: groupEnabled
})
.then(res => {
isSaving(false);
setError("");
closeModalAndRefresh();
})
.catch(err => {
isSaving(false);
setError(err);
});
} else {
api
.invoke("POST", "/api/v1/groups", {
group: groupName,
members: selectedUsers
})
.then(res => {
isSaving(false);
setError("");
closeModalAndRefresh();
})
.catch(err => {
isSaving(false);
setError(err);
});
}
};
saveRecord();
}
}, [saving]);
}, [
saving,
groupName,
selectedUsers,
groupEnabled,
selectedGroup,
closeModalAndRefresh
]);
useEffect(() => {
if (selectedGroup && loadingGroup) {
const fetchGroupInfo = () => {
api
.invoke("GET", `/api/v1/groups/${selectedGroup}`)
.then((res: MainGroupProps) => {
setGroupEnabled(res.status);
setGroupName(res.name);
setSelectedUsers(res.members);
})
.catch(err => {
setError(err);
isLoadingGroup(false);
});
};
fetchGroupInfo();
}
}, [loadingGroup]);
}, [loadingGroup, selectedGroup]);
//Fetch Actions
const setSaving = (event: React.FormEvent) => {
@@ -104,152 +151,89 @@ const AddGroup = ({
isSaving(true);
};
const saveRecord = () => {
if (selectedGroup !== null) {
api
.invoke("PUT", `/api/v1/groups/${groupName}`, {
group: groupName,
members: selectedUsers,
status: groupEnabled
})
.then(res => {
isSaving(false);
setError("");
closeModalAndRefresh();
})
.catch(err => {
isSaving(false);
setError(err);
});
} else {
api
.invoke("POST", "/api/v1/groups", {
group: groupName,
members: selectedUsers
})
.then(res => {
isSaving(false);
setError("");
closeModalAndRefresh();
})
.catch(err => {
isSaving(false);
setError(err);
});
}
};
const fetchGroupInfo = () => {
api
.invoke("GET", `/api/v1/groups/${selectedGroup}`)
.then((res: MainGroupProps) => {
setGroupEnabled(res.status);
setGroupName(res.name);
setSelectedUsers(res.members);
})
.catch(err => {
setError(err);
isLoadingGroup(false);
});
};
return (
<Dialog
open={open}
<ModalWrapper
modalOpen={open}
onClose={closeModalAndRefresh}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
title={selectedGroup !== null ? `Group Edit - ${groupName}` : "Add Group"}
>
<DialogTitle id="alert-dialog-title">
{selectedGroup !== null ? `Group Edit - ${groupName}` : "Add Group"}
</DialogTitle>
<DialogContent>
<form noValidate autoComplete="off" onSubmit={setSaving}>
<Grid container>
{addError !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
</Grid>
)}
{selectedGroup !== null ? (
<React.Fragment>
<Grid item xs={12}>
<Title>Status</Title>
<RadioGroup
aria-label="status"
name="status"
value={groupEnabled}
onChange={e => {
setGroupEnabled(e.target.value);
}}
>
<FormControlLabel
value="enabled"
control={<Radio color={"primary"} />}
label="Enabled"
/>
<FormControlLabel
value="disabled"
control={<Radio color={"primary"} />}
label="Disabled"
/>
</RadioGroup>
</Grid>
</React.Fragment>
) : (
<React.Fragment>
<Grid item xs={12}>
<TextField
id="standard-basic"
fullWidth
label="Name"
value={groupName}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setGroupName(e.target.value);
}}
/>
</Grid>
</React.Fragment>
)}
<form noValidate autoComplete="off" onSubmit={setSaving}>
<Grid container>
{addError !== "" && (
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<UsersSelectors
selectedUsers={selectedUsers}
setSelectedUsers={setSelectedUsers}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<Button
type="submit"
variant="contained"
color="primary"
fullWidth
disabled={saving}
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
Save
</Button>
{addError}
</Typography>
</Grid>
{saving && (
)}
{selectedGroup !== null ? (
<React.Fragment>
<Grid item xs={12}>
<LinearProgress />
<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>
</form>
</DialogContent>
</Dialog>
<Grid item xs={12}>
<UsersSelectors
selectedUsers={selectedUsers}
setSelectedUsers={setSelectedUsers}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>
<Button
type="submit"
variant="contained"
color="primary"
disabled={saving}
>
Save
</Button>
</Grid>
{saving && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid>
</form>
</ModalWrapper>
);
};

View File

@@ -54,28 +54,27 @@ const DeleteGroup = ({
useEffect(() => {
if (isDeleting) {
const removeRecord = () => {
if (!selectedGroup) {
return;
}
api
.invoke("DELETE", `/api/v1/groups/${selectedGroup}`)
.then((res: UsersList) => {
setDeleteLoading(false);
setError("");
closeDeleteModalAndRefresh(true);
})
.catch(err => {
setDeleteLoading(false);
setError(err);
});
};
removeRecord();
}
}, [isDeleting]);
const removeRecord = () => {
if (!selectedGroup) {
return;
}
api
.invoke("DELETE", `/api/v1/groups/${selectedGroup}`)
.then((res: UsersList) => {
setDeleteLoading(false);
setError("");
closeDeleteModalAndRefresh(true);
})
.catch(err => {
setDeleteLoading(false);
setError(err);
});
};
}, [isDeleting, selectedGroup, closeDeleteModalAndRefresh]);
const closeNoAction = () => {
setError("");

View File

@@ -14,37 +14,22 @@
// 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 React, { useEffect, useState } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import TextField from "@material-ui/core/TextField";
import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
import {
Button,
IconButton,
LinearProgress,
TableFooter,
TablePagination
} from "@material-ui/core";
import Paper from "@material-ui/core/Paper";
import Table from "@material-ui/core/Table";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TableCell from "@material-ui/core/TableCell";
import TableBody from "@material-ui/core/TableBody";
import Checkbox from "@material-ui/core/Checkbox";
import ViewIcon from "@material-ui/icons/Visibility";
import DeleteIcon from "@material-ui/icons/Delete";
import { Button } from "@material-ui/core";
import { CreateIcon } from "../../../icons";
import api from "../../../common/api";
import { MinTablePaginationActions } from "../../../common/MinTablePaginationActions";
import { GroupsList } from "./types";
import { groupsSort, usersSort } from "../../../utils/sortFunctions";
import { UsersList } from "../Users/types";
import { groupsSort } from "../../../utils/sortFunctions";
import AddGroup from "../Groups/AddGroup";
import DeleteGroup from "./DeleteGroup";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
interface IGroupsProps {
classes: any;
@@ -134,31 +119,35 @@ const Groups = ({ classes }: IGroupsProps) => {
useEffect(() => {
if (loading) {
const fetchRecords = () => {
const offset = page * rowsPerPage;
api
.invoke("GET", `/api/v1/groups?offset=${offset}&limit=${rowsPerPage}`)
.then((res: GroupsList) => {
let resGroups: string[] = [];
if (res.groups !== null) {
resGroups = res.groups.sort(groupsSort);
}
setRecords(resGroups);
const total = !res.total ? 0 : res.total;
setTotalRecords(total);
setError("");
isLoading(false);
// if we get 0 results, and page > 0 , go down 1 page
if ((!res.groups || res.groups.length === 0) && page > 0) {
const newPage = page - 1;
setPage(newPage);
}
})
.catch(err => {
setError(err);
isLoading(false);
});
};
fetchRecords();
}
}, [loading]);
const fetchRecords = () => {
const offset = page * rowsPerPage;
api
.invoke("GET", `/api/v1/groups?offset=${offset}&limit=${rowsPerPage}`)
.then((res: GroupsList) => {
setRecords(res.groups.sort(groupsSort));
setTotalRecords(res.total);
setError("");
isLoading(false);
// if we get 0 results, and page > 0 , go down 1 page
if ((!res.groups || res.groups.length === 0) && page > 0) {
const newPage = page - 1;
setPage(newPage);
}
})
.catch(err => {
setError(err);
isLoading(false);
});
};
}, [loading, page, rowsPerPage]);
const closeAddModalAndRefresh = () => {
setGroupOpen(false);
@@ -177,6 +166,21 @@ const Groups = ({ classes }: IGroupsProps) => {
elementItem.includes(filter)
);
const viewAction = (group: any) => {
setGroupOpen(true);
setSelectedGroup(group);
};
const deleteAction = (group: any) => {
setDeleteOpen(true);
setSelectedGroup(group);
};
const tableActions = [
{ type: "view", onClick: viewAction },
{ type: "delete", onClick: deleteAction }
];
return (
<React.Fragment>
{addGroupOpen && (
@@ -200,6 +204,7 @@ const Groups = ({ classes }: IGroupsProps) => {
<Grid item xs={12}>
<br />
</Grid>
{error !== "" ? <Grid container>{error}</Grid> : <React.Fragment />}
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Groups"
@@ -235,68 +240,28 @@ const Groups = ({ classes }: IGroupsProps) => {
<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>Name</TableCell>
<TableCell align="right">Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{filteredRecords.map(group => (
<TableRow key={`user-${group}`}>
<TableCell className={classes.wrapCell}>
{group}
</TableCell>
<TableCell align="right">
<IconButton
aria-label="view"
onClick={() => {
setGroupOpen(true);
setSelectedGroup(group);
}}
>
<ViewIcon />
</IconButton>
<IconButton
aria-label="delete"
onClick={() => {
setDeleteOpen(true);
setSelectedGroup(group);
}}
>
<DeleteIcon />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={[5, 10, 25]}
colSpan={3}
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 Groups Available</div>
)}
</Paper>
<TableWrapper
itemActions={tableActions}
columns={[{ label: "Name", elementKey: "" }]}
isLoading={loading}
records={filteredRecords}
entityName="Groups"
idField=""
paginatorConfig={{
rowsPerPageOptions: [5, 10, 25],
colSpan: 3,
count: totalRecords,
rowsPerPage: rowsPerPage,
page: page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true
},
onChangePage: handleChangePage,
onChangeRowsPerPage: handleChangeRowsPerPage,
ActionsComponent: MinTablePaginationActions
}}
/>
</Grid>
</Grid>
</React.Fragment>

View File

@@ -15,24 +15,19 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useState, useEffect } from "react";
import get from "lodash/get";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { LinearProgress } from "@material-ui/core";
import TableContainer from "@material-ui/core/TableContainer";
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 Title from "../../../common/Title";
import Checkbox from "@material-ui/core/Checkbox";
import { UsersList } from "../Users/types";
import { usersSort } from "../../../utils/sortFunctions";
import api from "../../../common/api";
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";
interface IGroupsProps {
classes: any;
@@ -86,7 +81,8 @@ const styles = (theme: Theme) =>
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012",
width: "100%"
width: "100%",
zIndex: 500
},
noFound: {
textAlign: "center",
@@ -96,7 +92,7 @@ const styles = (theme: Theme) =>
maxHeight: 250
},
stickyHeader: {
backgroundColor: "transparent"
backgroundColor: "#fff"
}
});
@@ -122,13 +118,15 @@ const UsersSelectors = ({
}
}, [loading]);
const selUsers = !selectedUsers ? [] : selectedUsers;
//Fetch Actions
const selectionChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
const targetD = e.target;
const value = targetD.value;
const checked = targetD.checked;
let elements: string[] = [...selectedUsers]; // We clone the selectedGroups array
let elements: string[] = [...selUsers]; // We clone the selectedGroups array
if (checked) {
// If the user has checked this field we need to push this to selectedGroupsList
@@ -146,7 +144,13 @@ const UsersSelectors = ({
api
.invoke("GET", `/api/v1/users`)
.then((res: UsersList) => {
setRecords(res.users.sort(usersSort));
let users = get(res, "users", []);
if (!users) {
users = [];
}
setRecords(users.sort(usersSort));
setError("");
isLoading(false);
})
@@ -163,6 +167,7 @@ const UsersSelectors = ({
return (
<React.Fragment>
<Title>Members</Title>
{error !== "" ? <div>{error}</div> : <React.Fragment />}
<Grid item xs={12}>
<Paper className={classes.paper}>
{loading && <LinearProgress />}
@@ -187,40 +192,17 @@ const UsersSelectors = ({
}}
/>
</Grid>
<TableContainer className={classes.tableContainer}>
<Table size="small" stickyHeader aria-label="sticky table">
<TableHead className={classes.minTableHeader}>
<TableRow>
<TableCell className={classes.stickyHeader}>
Select
</TableCell>
<TableCell className={classes.stickyHeader}>
Access Key
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{filteredRecords.map(row => (
<TableRow key={`group-${row.accessKey}`}>
<TableCell padding="checkbox">
<Checkbox
value={row.accessKey}
color="primary"
inputProps={{
"aria-label": "secondary checkbox"
}}
onChange={selectionChanged}
checked={selectedUsers.includes(row.accessKey)}
/>
</TableCell>
<TableCell className={classes.wrapCell}>
{row.accessKey}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<Grid item xs={12}>
<TableWrapper
columns={[{ label: "Access Key", elementKey: "accessKey" }]}
onSelect={selectionChanged}
selectedItems={selUsers}
isLoading={loading}
records={filteredRecords}
entityName="Users"
idField="accessKey"
/>
</Grid>
</React.Fragment>
) : (
<div className={classes.noFound}>No Users Available</div>

View File

@@ -19,7 +19,7 @@ import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import ListItemText from "@material-ui/core/ListItemText";
import { NavLink } from "react-router-dom";
import { Divider, withStyles } from "@material-ui/core";
import { Divider, Typography, withStyles } from "@material-ui/core";
import { ExitToApp } from "@material-ui/icons";
import { AppState } from "../../store";
import { connect } from "react-redux";
@@ -27,7 +27,7 @@ import { userLoggedIn } from "../../actions";
import List from "@material-ui/core/List";
import storage from "local-storage-fallback";
import history from "../../history";
import logo from "../../icons/mkube_logo_temp.svg";
import logo from "../../icons/minio_console_logo.svg";
import {
BucketsIcon,
DashboardIcon,
@@ -36,6 +36,10 @@ import {
} from "../../icons";
import { createStyles, Theme } from "@material-ui/core/styles";
import PersonIcon from "@material-ui/icons/Person";
import api from "../../common/api";
import NotificationsIcon from "@material-ui/icons/Notifications";
import ListAltIcon from "@material-ui/icons/ListAlt";
import LoopIcon from "@material-ui/icons/Loop";
const styles = (theme: Theme) =>
createStyles({
@@ -44,7 +48,7 @@ const styles = (theme: Theme) =>
marginBottom: "20px",
textAlign: "center",
"& img": {
width: "76px"
width: "120px"
}
},
menuList: {
@@ -91,9 +95,20 @@ interface MenuProps {
class Menu extends React.Component<MenuProps> {
logout() {
storage.removeItem("token");
this.props.userLoggedIn(false);
history.push("/");
const deleteSession = () => {
storage.removeItem("token");
this.props.userLoggedIn(false);
history.push("/");
};
api
.invoke("POST", `/api/v1/logout`)
.then(() => {
deleteSession();
})
.catch((err: any) => {
console.log(err);
deleteSession();
});
}
render() {
@@ -116,12 +131,8 @@ class Menu extends React.Component<MenuProps> {
</ListItemIcon>
<ListItemText primary="Buckets" />
</ListItem>
<ListItem button component={NavLink} to="/policies">
<ListItemIcon>
<PermissionIcon />
</ListItemIcon>
<ListItemText primary="IAM Policies" />
</ListItem>
<Divider />
<ListItem component={Typography}>Admin</ListItem>
<ListItem button component={NavLink} to="/users">
<ListItemIcon>
<PersonIcon />
@@ -134,6 +145,31 @@ class Menu extends React.Component<MenuProps> {
</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 />
<ListItem
button

View File

@@ -0,0 +1,368 @@
// This file is part of MinIO Console Server
// Copyright (c) 2019 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, { useCallback, useEffect, useState } from "react";
import get from "lodash/get";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Button, LinearProgress } from "@material-ui/core";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
import ConfPostgres from "../Configurations/CustomForms/ConfPostgres";
import api from "../../../common/api";
import { serverNeedsRestart } from "../../../actions";
import { connect } from "react-redux";
import ConfMySql from "../Configurations/CustomForms/ConfMySql";
import ConfTargetGeneric from "../Configurations/ConfTargetGeneric";
import {
notificationEndpointsFields,
notifyPostgres,
notifyMysql,
notifyKafka,
notifyAmqp,
notifyMqtt,
notifyRedis,
notifyNats,
notifyElasticsearch,
notifyWebhooks,
notifyNsq,
removeEmptyFields
} from "../Configurations/utils";
import { IElementValue } from "../Configurations/types";
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
},
strongText: {
fontWeight: 700
},
keyName: {
marginLeft: 5
},
buttonContainer: {
textAlign: "right"
},
logoButton: {
height: "80px"
}
});
interface IAddNotificationEndpointProps {
open: boolean;
closeModalAndRefresh: any;
serverNeedsRestart: typeof serverNeedsRestart;
classes: any;
}
const AddNotificationEndpoint = ({
open,
closeModalAndRefresh,
serverNeedsRestart,
classes
}: IAddNotificationEndpointProps) => {
//Local States
const [service, setService] = useState<string>("");
const [valuesArr, setValueArr] = useState<IElementValue[]>([]);
const [saving, setSaving] = useState<boolean>(false);
const [addError, setError] = useState<string>("");
//Effects
useEffect(() => {
if (saving) {
const payload = {
key_values: removeEmptyFields(valuesArr)
};
api
.invoke("PUT", `/api/v1/configs/${service}`, payload)
.then(res => {
setSaving(false);
setError("");
serverNeedsRestart(true);
closeModalAndRefresh();
})
.catch(err => {
setSaving(false);
setError(err);
});
}
}, [saving, serverNeedsRestart, service, valuesArr, closeModalAndRefresh]);
//Fetch Actions
const submitForm = (event: React.FormEvent) => {
event.preventDefault();
setSaving(true);
};
const onValueChange = useCallback(
newValue => {
setValueArr(newValue);
},
[setValueArr]
);
let srvComponent = <React.Fragment />;
switch (service) {
case notifyPostgres: {
srvComponent = <ConfPostgres onChange={onValueChange} />;
break;
}
case notifyMysql: {
srvComponent = <ConfMySql onChange={onValueChange} />;
break;
}
default: {
const fields = get(notificationEndpointsFields, service, []);
srvComponent = (
<ConfTargetGeneric fields={fields} onChange={onValueChange} />
);
}
}
let targetTitle = "";
switch (service) {
case notifyNsq:
targetTitle = "NSQ";
break;
case notifyWebhooks:
targetTitle = "Webhooks";
break;
case notifyElasticsearch:
targetTitle = "Elastic Search";
break;
case notifyNats:
targetTitle = "NATS";
break;
case notifyMqtt:
targetTitle = "MQTT";
break;
case notifyRedis:
targetTitle = "Redis";
break;
case notifyKafka:
targetTitle = "Kafka";
break;
case notifyPostgres:
targetTitle = "Postgres";
break;
case notifyMysql:
targetTitle = "Mysql";
break;
case notifyAmqp:
targetTitle = "AMQP";
break;
}
return (
<ModalWrapper
modalOpen={open}
onClose={closeModalAndRefresh}
title={`Add Lambda Notification Target ${targetTitle}`}
>
{service === "" && (
<Grid container>
<Grid item xs={12}>
<p>Pick a supported service:</p>
<table className={classes.chooseTable} style={{ width: "100%" }}>
<tbody>
<tr>
<td>
<Button
onClick={() => {
setService(notifyPostgres);
}}
>
<img
src="/postgres.png"
className={classes.logoButton}
alt="postgres"
/>
</Button>
</td>
<td>
<Button
onClick={() => {
setService(notifyKafka);
}}
>
<img
src="/kafka.png"
className={classes.logoButton}
alt="kafka"
/>
</Button>
</td>
<td>
<Button
onClick={() => {
setService(notifyAmqp);
}}
>
<img
src="/amqp.png"
className={classes.logoButton}
alt="amqp"
/>
</Button>
</td>
</tr>
<tr>
<td>
<Button
onClick={() => {
setService(notifyMqtt);
}}
>
<img
src="/mqtt.png"
className={classes.logoButton}
alt="mqtt"
/>
</Button>
</td>
<td>
<Button
onClick={() => {
setService(notifyRedis);
}}
>
<img
src="/redis.png"
className={classes.logoButton}
alt="redis"
/>
</Button>
</td>
<td>
<Button
onClick={() => {
setService(notifyNats);
}}
>
<img
src="/nats.png"
className={classes.logoButton}
alt="nats"
/>
</Button>
</td>
</tr>
<tr>
<td>
<Button
onClick={() => {
setService(notifyMysql);
}}
>
<img
src="/mysql.png"
className={classes.logoButton}
alt="mysql"
/>
</Button>
</td>
<td>
<Button
onClick={() => {
setService(notifyElasticsearch);
}}
>
<img
src="/elasticsearch.png"
className={classes.logoButton}
alt="elasticsearch"
/>
</Button>
</td>
<td></td>
</tr>
<tr>
<td>
<Button
onClick={() => {
setService(notifyWebhooks);
}}
>
Webhook
</Button>
</td>
<td>
<Button
onClick={() => {
setService(notifyNsq);
}}
>
NSQ
</Button>
</td>
<td />
</tr>
</tbody>
</table>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
{saving && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid>
)}
{service !== "" && (
<React.Fragment>
{addError !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
</Grid>
)}
<form noValidate onSubmit={submitForm}>
{srvComponent}
<Grid item xs={3} className={classes.buttonContainer}>
<Button
type="submit"
variant="contained"
color="primary"
fullWidth
disabled={saving}
>
Save
</Button>
</Grid>
<Grid item xs={9} />
</form>
</React.Fragment>
)}
</ModalWrapper>
);
};
const connector = connect(null, { serverNeedsRestart });
export default connector(withStyles(styles)(AddNotificationEndpoint));

View File

@@ -0,0 +1,244 @@
// 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 { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { TextField } from "@material-ui/core";
import { red } from "@material-ui/core/colors";
import Grid from "@material-ui/core/Grid";
import Button from "@material-ui/core/Button";
import Typography from "@material-ui/core/Typography";
import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
import { MinTablePaginationActions } from "../../../common/MinTablePaginationActions";
import {
NotificationEndpointItem,
NotificationEndpointsList,
TransformedEndpointItem
} from "./types";
import { notificationTransform } from "./utils";
import { CreateIcon } from "../../../icons";
import api from "../../../common/api";
import FiberManualRecordIcon from "@material-ui/icons/FiberManualRecord";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import AddNotificationEndpoint from "./AddNotificationEndpoint";
interface IListNotificationEndpoints {
classes: any;
}
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
},
strongText: {
fontWeight: 700
},
keyName: {
marginLeft: 5
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10
}
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012"
},
iconText: {
lineHeight: "24px"
}
});
const ListNotificationEndpoints = ({ classes }: IListNotificationEndpoints) => {
//Local States
const [records, setRecords] = useState<TransformedEndpointItem[]>([]);
const [totalRecords, setTotalRecords] = useState<number>(0);
const [rowsPerPage, setRowsPerPage] = useState<number>(10);
const [page, setPage] = useState<number>(0);
const [filter, setFilter] = useState<string>("");
const [error, setError] = useState<string>("");
const [isLoading, setIsLoading] = useState<boolean>(false);
const [addScreenOpen, setAddScreenOpen] = useState<boolean>(false);
//Effects
// load records on mount
useEffect(() => {
if (isLoading) {
const fetchRecords = () => {
api
.invoke("GET", `/api/v1/admin/notification_endpoints`)
.then((res: NotificationEndpointsList) => {
let resNotEndList: NotificationEndpointItem[] = [];
if (res.notification_endpoints !== null) {
resNotEndList = res.notification_endpoints;
}
setRecords(notificationTransform(resNotEndList));
setTotalRecords(resNotEndList.length);
setError("");
setIsLoading(false);
})
.catch(err => {
setError(err);
setIsLoading(false);
});
};
fetchRecords();
}
}, [isLoading]);
useEffect(() => {
setIsLoading(true);
}, []);
const tableActions = [
{ type: "view", to: "/notification-endpoints", sendOnlyId: true },
{
type: "delete",
onClick: (row: any) => {
//confirmDeleteBucket(row.name);
}
}
];
const filteredRecords = records.filter((b: TransformedEndpointItem) => {
if (filter === "") {
return true;
} else {
if (b.service_name.indexOf(filter) >= 0) {
return true;
} else {
return false;
}
}
});
const statusDisplay = (status: string) => {
return (
<div
style={{
display: "flex",
alignItems: "center"
}}
>
<FiberManualRecordIcon
style={status === "Offline" ? { color: red[500] } : {}}
/>
{status}
</div>
);
};
return (
<React.Fragment>
{addScreenOpen && (
<AddNotificationEndpoint
open={addScreenOpen}
closeModalAndRefresh={() => {
setIsLoading(true);
setAddScreenOpen(false);
}}
/>
)}
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">Lambda Notification Targets</Typography>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
{error !== "" && <Grid container>{error}</Grid>}
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Filter"
className={classes.searchField}
id="search-resource"
label=""
onChange={event => {
setFilter(event.target.value);
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddScreenOpen(true);
}}
>
Add Notification Target
</Button>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<TableWrapper
itemActions={tableActions}
columns={[
{ label: "Service", elementKey: "service_name" },
{
label: "Status",
elementKey: "status",
renderFunction: statusDisplay
}
]}
isLoading={isLoading}
records={filteredRecords}
entityName="Notification Endpoints"
idField="service_name"
paginatorConfig={{
rowsPerPageOptions: [5, 10, 25],
colSpan: 3,
count: totalRecords,
rowsPerPage: rowsPerPage,
page: page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true
},
onChangePage: (event: unknown, newPage: number) => {
setPage(newPage);
},
onChangeRowsPerPage: (
event: React.ChangeEvent<HTMLInputElement>
) => {
const rPP = parseInt(event.target.value, 10);
setRowsPerPage(rPP);
},
ActionsComponent: MinTablePaginationActions
}}
/>
</Grid>
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(ListNotificationEndpoints);

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 NotificationEndpoint {
service: string;
account_id: string;
properties: Map<string, string>;
}
export interface NotificationEndpointItem {
service: string;
account_id: string;
status: string;
}
export interface TransformedEndpointItem {
service_name: string;
status: string;
}
export interface NotificationEndpointsList {
notification_endpoints: NotificationEndpointItem[];
}

View File

@@ -0,0 +1,28 @@
// 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 { NotificationEndpointItem } from "./types";
export const notificationTransform = (
notificationElements: NotificationEndpointItem[]
) => {
return notificationElements.map(element => {
return {
service_name: `${element.service}:${element.account_id}`,
status: element.status
};
});
};

View File

@@ -483,7 +483,6 @@ class AddPermissionContent extends React.Component<
count={buckets.length}
rowsPerPage={rowsPerPage}
page={page}
labelRowsPerPage={null}
onChangePage={handleChangePage}
onChangeRowsPerPage={handleChangeRowsPerPage}
/>
@@ -521,28 +520,28 @@ class AddPermissionContent extends React.Component<
label="Write Only"
/>
<FormControlLabel
value="trace"
control={<Radio />}
label="Trace"
value="trace"
control={<Radio />}
label="Trace"
/>
</RadioGroup>
</FormControl>
</Grid>
{action === 'trace' && (
<React.Fragment>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
Trace displays tracing information for all buckets.
</Typography>
</Grid>
</React.Fragment>
{action === "trace" && (
<React.Fragment>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
Trace displays tracing information for all buckets.
</Typography>
</Grid>
</React.Fragment>
)}
<Grid item xs={12}>
<br />

View File

@@ -40,13 +40,11 @@ import { MinTablePaginationActions } from "../../../common/MinTablePaginationAct
import EditIcon from "@material-ui/icons/Edit";
import Checkbox from "@material-ui/core/Checkbox";
import { CreateIcon } from "../../../icons";
import TextField from '@material-ui/core/TextField';
import InputBase from '@material-ui/core/InputBase';
import SearchIcon from '@material-ui/icons/Search';
import InputAdornment from '@material-ui/core/InputAdornment';
import TextField from "@material-ui/core/TextField";
import SearchIcon from "@material-ui/icons/Search";
import InputAdornment from "@material-ui/core/InputAdornment";
import PlayArrowRoundedIcon from "@material-ui/icons/PlayArrowRounded";
const styles = (theme: Theme) =>
createStyles({
seeMore: {
@@ -84,14 +82,14 @@ const styles = (theme: Theme) =>
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"
}
});
@@ -241,7 +239,6 @@ class Permissions extends React.Component<
return (
<React.Fragment>
<AddPermission
open={addScreenOpen}
selectedPermission={selectedPermission}
@@ -269,7 +266,7 @@ class Permissions extends React.Component<
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
)
}}
/>
<Button
@@ -324,7 +321,7 @@ class Permissions extends React.Component<
<Checkbox
value="secondary"
color="primary"
inputProps={{ 'aria-label': 'secondary checkbox' }}
inputProps={{ "aria-label": "secondary checkbox" }}
/>
</TableCell>
<TableCell className={classes.wrapCell}>

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