Compare commits

...

20 Commits

Author SHA1 Message Date
Daniel Valdivia
a017c71d20 Release v0.16.1 (#1928)
Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
2022-04-29 18:55:43 -07:00
Harshavardhana
4001f14953 keep lookupMap for easy reading for relevant publicKey file (#1930)
Co-authored-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
2022-04-29 15:14:04 -07:00
Cesar Celis Hernandez
130413cbef having idp configured via env variable only (#1931) 2022-04-29 14:48:21 -07:00
Lenin Alevski
0622cc658b fix: parseTenantCertificates was ignoring cert-manager secrets (#1929)
Signed-off-by: Lenin Alevski <alevsk.8772@gmail.com>

Co-authored-by: Cesar Celis Hernandez <celis.hernandez.cesar@gmail.com>
2022-04-29 11:55:07 -07:00
Alex
663a5b196d Remove of suspended traffic warning in profile page (#1926) 2022-04-29 01:45:42 -05:00
Prakash Senthil Vel
6f676f73a4 UX Prometheus dashboard (#1907) 2022-04-28 19:57:16 -07:00
Daniel Valdivia
26d7001ae1 Remove all un-used CSS classes (#1924)
Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
2022-04-28 19:33:46 -05:00
Daniel Valdivia
f79a8e8177 FormLayout Component (#1922)
Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
2022-04-28 15:13:20 -07:00
Daniel Valdivia
0e5147bb1d Constraint PageLayout to max 1220px on large resolutions (#1919)
Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
2022-04-28 14:34:29 -07:00
Lenin Alevski
566fb27fc1 Error and Audit logger webhooks (#1855)
Similar to MinIO now it's possible to configure webhooks to log all
triggered errors and incomming requests via env variables:

```
CONSOLE_LOGGER_WEBHOOK_ENABLE_<ID>
CONSOLE_LOGGER_WEBHOOK_ENDPOINT_<ID>
CONSOLE_LOGGER_WEBHOOK_AUTH_TOKEN_<ID>
CONSOLE_LOGGER_WEBHOOK_CLIENT_CERT_<ID>
CONSOLE_LOGGER_WEBHOOK_CLIENT_KEY_<ID>
CONSOLE_LOGGER_WEBHOOK_QUEUE_SIZE_<ID>

CONSOLE_AUDIT_WEBHOOK_ENABLE_<ID>
CONSOLE_AUDIT_WEBHOOK_ENDPOINT_<ID>
CONSOLE_AUDIT_WEBHOOK_AUTH_TOKEN_<ID>
CONSOLE_AUDIT_WEBHOOK_CLIENT_CERT_<ID>
CONSOLE_AUDIT_WEBHOOK_QUEUE_SIZE_<ID>
```

Signed-off-by: Lenin Alevski <alevsk.8772@gmail.com>
2022-04-28 12:55:06 -07:00
Lenin Alevski
8c18829089 Update DNS Rebinding etcd vulnerable dependency (#1918)
Signed-off-by: Lenin Alevski <alevsk.8772@gmail.com>
2022-04-28 14:21:17 -05:00
adfost
c68c175827 committing new tests (#1879) 2022-04-28 11:52:37 -05:00
dependabot[bot]
415088ae2d Bump ejs from 3.1.6 to 3.1.7 in /portal-ui (#1915)
Bumps [ejs](https://github.com/mde/ejs) from 3.1.6 to 3.1.7.
- [Release notes](https://github.com/mde/ejs/releases)
- [Changelog](https://github.com/mde/ejs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mde/ejs/compare/v3.1.6...v3.1.7)

---
updated-dependencies:
- dependency-name: ejs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-28 11:00:34 -05:00
adfost
158926f192 groups_test (#1916) 2022-04-28 08:09:06 -07:00
Lenin Alevski
f026ffffc8 Fix profiling endpoint and adding tests (#1917)
Signed-off-by: Lenin Alevski <alevsk.8772@gmail.com>
2022-04-27 20:07:32 -07:00
Cesar Celis Hernandez
bd0edea3df Increase coverage threshold (#1914) 2022-04-27 16:49:58 -05:00
Alex
07b4dad4d3 Fixed issues with Quota modal (#1911)
Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
2022-04-27 14:08:19 -07:00
Cesar Celis Hernandez
0df796bc03 fixing integration tests to use proxy (#1912) 2022-04-27 12:33:10 -07:00
Daniel Valdivia
cf0212391e Console Swagger Module Reorganization (#1881)
Co-authored-by: Alex <33497058+bexsoft@users.noreply.github.com>
Co-authored-by: Prakash Senthil Vel <23444145+prakashsvmx@users.noreply.github.com>
2022-04-27 11:45:04 -07:00
Alex
cb3a695c25 Removed quota from versioning section & added inside summary page (#1910)
Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
2022-04-27 09:36:39 -07:00
1028 changed files with 11991 additions and 7606 deletions

View File

@@ -65,7 +65,7 @@ function main() {
check_tenant_status tenant-lite storage-lite
kubectl -n minio-operator port-forward svc/console 9090 &
kubectl proxy &
}
main "$@"

View File

@@ -1346,7 +1346,7 @@ jobs:
result=${result%\%}
echo "result:"
echo $result
threshold=36.6
threshold=41.2
if (( $(echo "$result >= $threshold" |bc -l) )); then
echo "It is equal or greater than threshold, passed!"
else

1
.gitignore vendored
View File

@@ -19,6 +19,7 @@ vendor/
# Ignore executables
target/
!pkg/logger/target/
console
!console/

View File

@@ -164,18 +164,12 @@ test-sso-integration:
-p 9000:9000 \
-p 9001:9001 \
-e MINIO_IDENTITY_OPENID_CLIENT_SECRET=0nfJuqIt0iPnRIUJkvetve5l38C6gi9W \
-e MINIO_IDENTITY_OPENID_CONFIG_URL=http://keycloak-container:8080/auth/realms/myrealm/.well-known/openid-configuration \
-e MINIO_IDENTITY_OPENID_CLIENT_ID="account" \
-e MINIO_ROOT_USER=minio \
-e MINIO_ROOT_PASSWORD=minio123 $(MINIO_VERSION) server /data{1...4} --address :9000 --console-address :9001)
@(sleep 60)
@echo "run mc commands"
@(docker run --name minio-client --network my-net -dit --entrypoint=/bin/sh minio/mc)
@(docker exec minio-client mc alias set myminio/ http://minio:9000 minio minio123)
@(docker exec minio-client mc admin config set myminio identity_openid config_url="http://keycloak-container:8080/auth/realms/myrealm/.well-known/openid-configuration" client_id="account")
@(docker exec minio-client mc admin service restart myminio)
@echo "starting bash script"
@(env bash $(PWD)/sso-integration/set-sso.sh)
@echo "install jq"
@(sudo apt install jq)
@echo "Executing the test:"
@(cd sso-integration && go test -coverpkg=../restapi -c -tags testrunmain . && mkdir -p coverage && ./sso-integration.test -test.v -test.run "^Test*" -test.coverprofile=coverage/sso-system.out)

View File

@@ -23,6 +23,8 @@ import (
"strings"
"time"
xhttp "github.com/minio/console/pkg/http"
"github.com/minio/console/pkg/utils"
"github.com/minio/pkg/env"
@@ -68,7 +70,7 @@ func GetMinioImage() (*string, error) {
return &image, nil
}
latestMinIOImage, errLatestMinIOImage := utils.GetLatestMinIOImage(
&utils.HTTPClient{
&xhttp.Client{
Client: &http.Client{
Timeout: 5 * time.Second,
},

View File

@@ -20,10 +20,14 @@
package main
import (
"context"
"fmt"
"os"
"strconv"
"time"
"github.com/minio/console/pkg/logger"
"github.com/minio/cli"
"github.com/minio/console/restapi"
)
@@ -36,15 +40,28 @@ var appCmds = []cli.Command{
// StartServer starts the console service
func StartServer(ctx *cli.Context) error {
if os.Getenv("CONSOLE_OPERATOR_MODE") != "" && os.Getenv("CONSOLE_OPERATOR_MODE") == "on" {
return startOperatorServer(ctx)
}
// Load all certificates
if err := loadAllCerts(ctx); err != nil {
// Log this as a warning and continue running console without TLS certificates
restapi.LogError("Unable to load certs: %v", err)
}
xctx := context.Background()
transport := restapi.PrepareSTSClientTransport(false)
if err := logger.InitializeLogger(xctx, transport); err != nil {
fmt.Println("error InitializeLogger", err)
logger.CriticalIf(xctx, err)
}
// custom error configuration
restapi.LogInfo = logger.Info
restapi.LogError = logger.Error
restapi.LogIf = logger.LogIf
if os.Getenv("CONSOLE_OPERATOR_MODE") != "" && os.Getenv("CONSOLE_OPERATOR_MODE") == "on" {
return startOperatorServer(ctx)
}
var rctx restapi.Context
if err := rctx.Load(ctx); err != nil {
restapi.LogError("argument validation failed: %v", err)

View File

@@ -20,9 +20,13 @@
package main
import (
"context"
"fmt"
"strconv"
"time"
"github.com/minio/console/pkg/logger"
"github.com/minio/cli"
"github.com/minio/console/restapi"
)
@@ -39,6 +43,17 @@ func StartServer(ctx *cli.Context) error {
restapi.LogError("Unable to load certs: %v", err)
}
xctx := context.Background()
transport := restapi.PrepareSTSClientTransport(false)
if err := logger.InitializeLogger(xctx, transport); err != nil {
fmt.Println("error InitializeLogger", err)
logger.CriticalIf(xctx, err)
}
// custom error configuration
restapi.LogInfo = logger.Info
restapi.LogError = logger.Error
restapi.LogIf = logger.LogIf
var rctx restapi.Context
if err := rctx.Load(ctx); err != nil {
restapi.LogError("argument validation failed: %v", err)

View File

@@ -2,7 +2,7 @@
// +build operator
// This file is part of MinIO Console Server
// Copyright (c) 2021 MinIO, Inc.
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
@@ -20,6 +20,7 @@
package main
import (
"context"
"fmt"
"io/ioutil"
"path/filepath"
@@ -27,6 +28,8 @@ import (
"syscall"
"time"
"github.com/minio/console/pkg/logger"
"github.com/minio/console/restapi"
"github.com/go-openapi/loads"
@@ -106,7 +109,7 @@ func buildOperatorServer() (*operatorapi.Server, error) {
}
api := operations.NewOperatorAPI(swaggerSpec)
api.Logger = operatorapi.LogInfo
api.Logger = restapi.LogInfo
server := operatorapi.NewServer(api)
parser := flags.NewParser(server, flags.Default)
@@ -147,7 +150,7 @@ func loadOperatorAllCerts(ctx *cli.Context) error {
}
// load the certificates and the CAs
operatorapi.GlobalRootCAs, operatorapi.GlobalPublicCerts, operatorapi.GlobalTLSCertsManager, err = certs.GetAllCertificatesAndCAs()
restapi.GlobalRootCAs, restapi.GlobalPublicCerts, restapi.GlobalTLSCertsManager, err = certs.GetAllCertificatesAndCAs()
if err != nil {
return fmt.Errorf("unable to load certificates at %s: failed with %w", certs.GlobalCertsDir.Get(), err)
}
@@ -159,12 +162,12 @@ func loadOperatorAllCerts(ctx *cli.Context) error {
swaggerServerCACertificate := ctx.String("tls-ca")
// load tls cert and key from swagger server tls-certificate and tls-key flags
if swaggerServerCertificate != "" && swaggerServerCertificateKey != "" {
if err = operatorapi.GlobalTLSCertsManager.AddCertificate(swaggerServerCertificate, swaggerServerCertificateKey); err != nil {
if err = restapi.GlobalTLSCertsManager.AddCertificate(swaggerServerCertificate, swaggerServerCertificateKey); err != nil {
return err
}
x509Certs, err := certs.ParsePublicCertFile(swaggerServerCertificate)
if err == nil {
operatorapi.GlobalPublicCerts = append(operatorapi.GlobalPublicCerts, x509Certs...)
restapi.GlobalPublicCerts = append(restapi.GlobalPublicCerts, x509Certs...)
}
}
@@ -172,7 +175,7 @@ func loadOperatorAllCerts(ctx *cli.Context) error {
if swaggerServerCACertificate != "" {
caCert, caCertErr := ioutil.ReadFile(swaggerServerCACertificate)
if caCertErr == nil {
operatorapi.GlobalRootCAs.AppendCertsFromPEM(caCert)
restapi.GlobalRootCAs.AppendCertsFromPEM(caCert)
}
}
}
@@ -186,20 +189,32 @@ func loadOperatorAllCerts(ctx *cli.Context) error {
// StartServer starts the console service
func startOperatorServer(ctx *cli.Context) error {
if err := loadOperatorAllCerts(ctx); err != nil {
if err := loadAllCerts(ctx); err != nil {
// Log this as a warning and continue running console without TLS certificates
operatorapi.LogError("Unable to load certs: %v", err)
restapi.LogError("Unable to load certs: %v", err)
}
xctx := context.Background()
transport := restapi.PrepareSTSClientTransport(false)
if err := logger.InitializeLogger(xctx, transport); err != nil {
fmt.Println("error InitializeLogger", err)
logger.CriticalIf(xctx, err)
}
// custom error configuration
restapi.LogInfo = logger.Info
restapi.LogError = logger.Error
restapi.LogIf = logger.LogIf
var rctx operatorapi.Context
if err := rctx.Load(ctx); err != nil {
operatorapi.LogError("argument validation failed: %v", err)
restapi.LogError("argument validation failed: %v", err)
return err
}
server, err := buildOperatorServer()
if err != nil {
operatorapi.LogError("Unable to initialize console server: %v", err)
restapi.LogError("Unable to initialize console server: %v", err)
return err
}
@@ -212,7 +227,7 @@ func startOperatorServer(ctx *cli.Context) error {
operatorapi.Port = strconv.Itoa(server.Port)
operatorapi.Hostname = server.Host
if len(operatorapi.GlobalPublicCerts) > 0 {
if len(restapi.GlobalPublicCerts) > 0 {
// If TLS certificates are provided enforce the HTTPS schema, meaning console will redirect
// plain HTTP connections to HTTPS server
server.EnabledListeners = []string{"http", "https"}

9
go.mod
View File

@@ -6,6 +6,7 @@ require (
github.com/blang/semver/v4 v4.0.0
github.com/cheggaaa/pb/v3 v3.0.8
github.com/dustin/go-humanize v1.0.0
github.com/fatih/color v1.13.0
github.com/go-openapi/errors v0.20.2
github.com/go-openapi/loads v0.21.1
github.com/go-openapi/runtime v0.23.3
@@ -14,10 +15,12 @@ require (
github.com/go-openapi/swag v0.21.1
github.com/go-openapi/validate v0.21.0
github.com/golang-jwt/jwt/v4 v4.4.1
github.com/google/uuid v1.3.0
github.com/gorilla/websocket v1.5.0
github.com/jessevdk/go-flags v1.5.0
github.com/klauspost/compress v1.15.1
github.com/minio/cli v1.22.0
github.com/minio/highwayhash v1.0.2
github.com/minio/kes v0.19.2
github.com/minio/madmin-go v1.3.12
github.com/minio/mc v0.0.0-20220419155441-cc4ff3a0cc82
@@ -57,7 +60,6 @@ require (
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/gdamore/encoding v1.0.0 // indirect
github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1 // indirect
@@ -74,7 +76,6 @@ require (
github.com/google/go-cmp v0.5.7 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/googleapis/gnostic v0.5.5 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
@@ -109,7 +110,7 @@ require (
github.com/navidys/tvxwidgets v0.1.0 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/philhofer/fwd v1.1.1 // indirect
github.com/philhofer/fwd v1.1.2-0.20210722190033-5c56ac6d0bb9 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pkg/xattr v0.4.5 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
@@ -126,7 +127,7 @@ require (
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tinylib/msgp v1.1.6 // indirect
github.com/tinylib/msgp v1.1.7-0.20211026165309-e818a1881b0e // indirect
github.com/tklauser/go-sysconf v0.3.10 // indirect
github.com/tklauser/numcpus v0.4.0 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect

861
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,83 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package integration
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func Test_ConfigAPI(t *testing.T) {
assert := assert.New(t)
type args struct {
api string
}
tests := []struct {
name string
args args
expectedStatus int
expectedError error
}{
{
name: "Config - Valid",
args: args{
api: "/configs",
},
expectedStatus: 200,
expectedError: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := &http.Client{
Timeout: 3 * time.Second,
}
requestDataPolicy := map[string]interface{}{}
requestDataJSON, _ := json.Marshal(requestDataPolicy)
requestDataBody := bytes.NewReader(requestDataJSON)
request, err := http.NewRequest(
"GET", fmt.Sprintf("http://localhost:9090/api/v1%s", tt.args.api), requestDataBody)
if err != nil {
log.Println(err)
return
}
request.Header.Add("Cookie", fmt.Sprintf("token=%s", token))
request.Header.Add("Content-Type", "application/json")
response, err := client.Do(request)
if err != nil {
log.Println(err)
return
}
if response != nil {
assert.Equal(tt.expectedStatus, response.StatusCode, tt.name+" Failed")
}
})
}
}

174
integration/groups_test.go Normal file
View File

@@ -0,0 +1,174 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package integration
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func Test_AddGroupAPI(t *testing.T) {
assert := assert.New(t)
AddUser("member1", "testtest", []string{}, []string{"consoleAdmin"})
type args struct {
api string
group string
members []string
}
tests := []struct {
name string
args args
expectedStatus int
expectedError error
}{
{
name: "Create Group - Valid",
args: args{
api: "/groups",
group: "test",
members: []string{"member1"},
},
expectedStatus: 201,
expectedError: nil,
},
{
name: "Create Group - Invalid",
args: args{
api: "/groups",
group: "test",
members: []string{},
},
expectedStatus: 400,
expectedError: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := &http.Client{
Timeout: 3 * time.Second,
}
// Add policy
requestDataPolicy := map[string]interface{}{}
requestDataPolicy["group"] = tt.args.group
requestDataPolicy["members"] = tt.args.members
requestDataJSON, _ := json.Marshal(requestDataPolicy)
requestDataBody := bytes.NewReader(requestDataJSON)
request, err := http.NewRequest(
"POST", fmt.Sprintf("http://localhost:9090/api/v1%s", tt.args.api), requestDataBody)
if err != nil {
log.Println(err)
return
}
request.Header.Add("Cookie", fmt.Sprintf("token=%s", token))
request.Header.Add("Content-Type", "application/json")
response, err := client.Do(request)
if err != nil {
log.Println(err)
return
}
if response != nil {
assert.Equal(tt.expectedStatus, response.StatusCode, "Status Code is incorrect")
}
})
}
}
func Test_DeleteGroupAPI(t *testing.T) {
assert := assert.New(t)
AddGroup("grouptests1", []string{})
type args struct {
api string
}
tests := []struct {
name string
args args
expectedStatus int
expectedError error
verb string
}{
{
name: "Delete Group - Valid",
verb: "DELETE",
args: args{
api: "/group?name=grouptests1",
},
expectedStatus: 204,
expectedError: nil,
},
{
name: "Access Group After Delete - Invalid",
verb: "GET",
args: args{
api: "/group?name=grouptests1",
},
expectedStatus: 500,
expectedError: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := &http.Client{
Timeout: 3 * time.Second,
}
// Add policy
requestDataPolicy := map[string]interface{}{}
requestDataJSON, _ := json.Marshal(requestDataPolicy)
requestDataBody := bytes.NewReader(requestDataJSON)
request, err := http.NewRequest(
tt.verb, fmt.Sprintf("http://localhost:9090/api/v1%s", tt.args.api), requestDataBody)
if err != nil {
log.Println(err)
return
}
request.Header.Add("Cookie", fmt.Sprintf("token=%s", token))
request.Header.Add("Content-Type", "application/json")
response, err := client.Do(request)
if err != nil {
log.Println(err)
return
}
if response != nil {
assert.Equal(tt.expectedStatus, response.StatusCode, "Status Code is incorrect")
}
})
}
}

View File

@@ -20,6 +20,7 @@ import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"testing"
@@ -30,7 +31,64 @@ import (
"github.com/stretchr/testify/assert"
)
func Test_PolicyAPI(t *testing.T) {
func AddPolicy(name string, definition string) (*http.Response, error) {
/*
This is an atomic function to add user and can be reused across
different functions.
*/
client := &http.Client{
Timeout: 3 * time.Second,
}
requestDataAdd := map[string]interface{}{
"name": name,
"policy": definition,
}
requestDataJSON, _ := json.Marshal(requestDataAdd)
requestDataBody := bytes.NewReader(requestDataJSON)
request, err := http.NewRequest(
"POST", "http://localhost:9090/api/v1/policies", requestDataBody)
if err != nil {
log.Println(err)
}
request.Header.Add("Cookie", fmt.Sprintf("token=%s", token))
request.Header.Add("Content-Type", "application/json")
response, err := client.Do(request)
return response, err
}
func SetPolicy(policies []string, entityName string, entityType string) (*http.Response, error) {
/*
This is an atomic function to add user and can be reused across
different functions.
*/
client := &http.Client{
Timeout: 3 * time.Second,
}
requestDataAdd := map[string]interface{}{
"name": policies,
"entityType": entityType,
"entityName": entityName,
}
requestDataJSON, _ := json.Marshal(requestDataAdd)
requestDataBody := bytes.NewReader(requestDataJSON)
request, err := http.NewRequest(
"PUT", "http://localhost:9090/api/v1/set-policy", requestDataBody)
if err != nil {
log.Println(err)
}
request.Header.Add("Cookie", fmt.Sprintf("token=%s", token))
request.Header.Add("Content-Type", "application/json")
response, err := client.Do(request)
return response, err
}
func Test_AddPolicyAPI(t *testing.T) {
assert := assert.New(t)
type args struct {
@@ -69,11 +127,12 @@ func Test_PolicyAPI(t *testing.T) {
expectedStatus: 201,
expectedError: nil,
},
{
name: "Create Policy - Invalid",
args: args{
api: "/policies",
name: "test",
name: "test2",
policy: swag.String(`
{
"Version": "2012-10-17",
@@ -103,11 +162,9 @@ func Test_PolicyAPI(t *testing.T) {
Timeout: 3 * time.Second,
}
// Add policy
requestDataPolicy := map[string]interface{}{}
requestDataPolicy["name"] = tt.args.name
if tt.args.policy != nil {
requestDataPolicy["name"] = tt.args.name
requestDataPolicy["policy"] = *tt.args.policy
}
@@ -127,10 +184,604 @@ func Test_PolicyAPI(t *testing.T) {
return
}
if response != nil {
assert.Equal(tt.expectedStatus, response.StatusCode, "Status Code is incorrect")
assert.Equal(tt.expectedStatus, response.StatusCode, tt.name+" Failed")
}
})
}
}
func Test_SetPolicyAPI(t *testing.T) {
assert := assert.New(t)
AddUser("policyuser1", "testtest", []string{}, []string{"readwrite"})
AddGroup("testgroup123", []string{})
AddPolicy("setpolicytest", `
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetBucketLocation",
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::*"
]
}
]
}`)
type args struct {
api string
entityType string
entityName string
policyName []string
}
tests := []struct {
name string
args args
expectedStatus int
expectedError error
}{
{
name: "Set Policy - Valid",
args: args{
api: "/set-policy",
policyName: []string{"setpolicytest"},
entityType: "user",
entityName: "policyuser1",
},
expectedStatus: 204,
expectedError: nil,
},
{
name: "Set Policy - Invalid",
args: args{
api: "/set-policy",
policyName: []string{"test3"},
entityType: "user",
entityName: "policyuser1",
},
expectedStatus: 500,
expectedError: nil,
},
{
name: "Set Policy Group - Valid",
args: args{
api: "/set-policy",
policyName: []string{"setpolicytest"},
entityType: "group",
entityName: "testgroup123",
},
expectedStatus: 204,
expectedError: nil,
},
{
name: "Set Policy Group - Invalid",
args: args{
api: "/set-policy",
policyName: []string{"test3"},
entityType: "group",
entityName: "testgroup123",
},
expectedStatus: 500,
expectedError: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := &http.Client{
Timeout: 3 * time.Second,
}
requestDataPolicy := map[string]interface{}{}
requestDataPolicy["entityName"] = tt.args.entityName
requestDataPolicy["entityType"] = tt.args.entityType
if tt.args.policyName != nil {
requestDataPolicy["name"] = tt.args.policyName
}
requestDataJSON, _ := json.Marshal(requestDataPolicy)
requestDataBody := bytes.NewReader(requestDataJSON)
request, err := http.NewRequest(
"PUT", fmt.Sprintf("http://localhost:9090/api/v1%s", tt.args.api), requestDataBody)
if err != nil {
log.Println(err)
return
}
request.Header.Add("Cookie", fmt.Sprintf("token=%s", token))
request.Header.Add("Content-Type", "application/json")
response, err := client.Do(request)
if err != nil {
log.Println(err)
return
}
if response != nil {
assert.Equal(tt.expectedStatus, response.StatusCode, tt.name+" Failed")
}
})
}
}
func Test_SetPolicyMultipleAPI(t *testing.T) {
assert := assert.New(t)
AddUser("policyuser2", "testtest", []string{}, []string{"readwrite"})
AddUser("policyuser3", "testtest", []string{}, []string{"readwrite"})
AddGroup("testgroup1234", []string{})
AddPolicy("setpolicytest2", `
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetBucketLocation",
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::*"
]
}
]
}`)
type args struct {
api string
users []string
groups []string
name []string
}
tests := []struct {
name string
args args
expectedStatus int
expectedError error
}{
{
name: "Set Policy - Valid",
args: args{
api: "/set-policy-multi",
name: []string{"setpolicytest2"},
users: []string{"policyuser2", "policyuser3"},
},
expectedStatus: 204,
expectedError: nil,
},
{
name: "Set Policy - Invalid",
args: args{
api: "/set-policy-multi",
name: []string{"test3"},
users: []string{"policyuser2", "policyuser3"},
},
expectedStatus: 500,
expectedError: nil,
},
{
name: "Set Policy Group - Valid",
args: args{
api: "/set-policy-multi",
name: []string{"setpolicytest2"},
groups: []string{"testgroup1234"},
},
expectedStatus: 204,
expectedError: nil,
},
{
name: "Set Policy Group - Valid",
args: args{
api: "/set-policy-multi",
name: []string{"setpolicytest23"},
groups: []string{"testgroup1234"},
},
expectedStatus: 500,
expectedError: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := &http.Client{
Timeout: 3 * time.Second,
}
requestDataPolicy := map[string]interface{}{}
requestDataPolicy["name"] = tt.args.name
requestDataPolicy["users"] = tt.args.users
requestDataPolicy["groups"] = tt.args.groups
requestDataJSON, _ := json.Marshal(requestDataPolicy)
requestDataBody := bytes.NewReader(requestDataJSON)
request, err := http.NewRequest(
"PUT", fmt.Sprintf("http://localhost:9090/api/v1%s", tt.args.api), requestDataBody)
if err != nil {
log.Println(err)
return
}
request.Header.Add("Cookie", fmt.Sprintf("token=%s", token))
request.Header.Add("Content-Type", "application/json")
response, err := client.Do(request)
if err != nil {
log.Println(err)
return
}
if response != nil {
assert.Equal(tt.expectedStatus, response.StatusCode, tt.name+" Failed")
}
})
}
}
func Test_ListPoliciesAPI(t *testing.T) {
assert := assert.New(t)
type args struct {
api string
}
tests := []struct {
name string
args args
expectedStatus int
expectedError error
}{
{
name: "List Policies",
args: args{
api: "/policies",
},
expectedStatus: 200,
expectedError: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := &http.Client{
Timeout: 3 * time.Second,
}
request, err := http.NewRequest(
"GET", fmt.Sprintf("http://localhost:9090/api/v1%s", tt.args.api), nil)
if err != nil {
log.Println(err)
return
}
request.Header.Add("Cookie", fmt.Sprintf("token=%s", token))
request.Header.Add("Content-Type", "application/json")
response, err := client.Do(request)
if err != nil {
log.Println(err)
return
}
if response != nil {
assert.Equal(tt.expectedStatus, response.StatusCode, tt.name+" Failed")
}
})
}
}
func Test_GetPolicyAPI(t *testing.T) {
assert := assert.New(t)
AddPolicy("getpolicytest", `
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetBucketLocation",
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::*"
]
}
]
}`)
type args struct {
api string
}
tests := []struct {
name string
args args
expectedStatus int
expectedError error
}{
{
name: "Get Policies - Invalid",
args: args{
api: "/policy?name=test3",
},
expectedStatus: 500,
expectedError: nil,
},
{
name: "Get Policies - Valid",
args: args{
api: "/policy?name=getpolicytest",
},
expectedStatus: 200,
expectedError: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := &http.Client{
Timeout: 3 * time.Second,
}
request, err := http.NewRequest(
"GET", fmt.Sprintf("http://localhost:9090/api/v1%s", tt.args.api), nil)
if err != nil {
log.Println(err)
return
}
request.Header.Add("Cookie", fmt.Sprintf("token=%s", token))
request.Header.Add("Content-Type", "application/json")
response, err := client.Do(request)
if err != nil {
log.Println(err)
return
}
if response != nil {
assert.Equal(tt.expectedStatus, response.StatusCode, tt.name+" Failed")
}
})
}
}
func Test_PolicyListUsersAPI(t *testing.T) {
assert := assert.New(t)
AddUser("policyuser4", "testtest", []string{}, []string{"readwrite"})
AddPolicy("policylistusers", `
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetBucketLocation",
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::*"
]
}
]
}`)
SetPolicy([]string{"policylistusers"}, "policyuser4", "user")
type args struct {
api string
}
tests := []struct {
name string
args args
expectedStatus int
expectedError error
}{
{
name: "List Users for Policy - Valid",
args: args{
api: "/policies/policylistusers/users",
},
expectedStatus: 200,
expectedError: nil,
},
{
name: "List Users for Policy - Invalid",
args: args{
api: "/policies/test2/users",
},
expectedStatus: 404,
expectedError: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := &http.Client{
Timeout: 3 * time.Second,
}
request, err := http.NewRequest(
"GET", fmt.Sprintf("http://localhost:9090/api/v1%s", tt.args.api), nil)
if err != nil {
log.Println(err)
return
}
request.Header.Add("Cookie", fmt.Sprintf("token=%s", token))
request.Header.Add("Content-Type", "application/json")
response, err := client.Do(request)
if err != nil {
log.Println(err)
return
}
if response != nil {
bodyBytes, _ := ioutil.ReadAll(response.Body)
assert.Equal(tt.expectedStatus, response.StatusCode, tt.name+" Failed")
if response.StatusCode == 200 {
assert.Equal("[\"policyuser4\"]\n", string(bodyBytes))
}
}
})
}
}
func Test_PolicyListGroupsAPI(t *testing.T) {
assert := assert.New(t)
AddGroup("testgroup12345", []string{})
AddPolicy("policylistgroups", `
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetBucketLocation",
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::*"
]
}
]
}`)
SetPolicy([]string{"policylistgroups"}, "testgroup12345", "group")
type args struct {
api string
}
tests := []struct {
name string
args args
expectedStatus int
expectedError error
}{
{
name: "List Users for Policy - Valid",
args: args{
api: "/policies/policylistgroups/groups",
},
expectedStatus: 200,
expectedError: nil,
},
{
name: "List Users for Policy - Invalid",
args: args{
api: "/policies/test3/groups",
},
expectedStatus: 404,
expectedError: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := &http.Client{
Timeout: 3 * time.Second,
}
request, err := http.NewRequest(
"GET", fmt.Sprintf("http://localhost:9090/api/v1%s", tt.args.api), nil)
if err != nil {
log.Println(err)
return
}
request.Header.Add("Cookie", fmt.Sprintf("token=%s", token))
request.Header.Add("Content-Type", "application/json")
response, err := client.Do(request)
if err != nil {
log.Println(err)
return
}
if response != nil {
bodyBytes, _ := ioutil.ReadAll(response.Body)
assert.Equal(tt.expectedStatus, response.StatusCode, tt.name+" Failed")
if response.StatusCode == 200 {
assert.Equal("[\"testgroup12345\"]\n", string(bodyBytes))
}
}
})
}
}
func Test_DeletePolicyAPI(t *testing.T) {
assert := assert.New(t)
AddPolicy("testdelete", `
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetBucketLocation",
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::*"
]
}
]
}`)
type args struct {
api string
method string
}
tests := []struct {
name string
args args
expectedStatus int
expectedError error
}{
{
name: "Delete Policies - Valid",
args: args{
api: "/policy?name=testdelete",
method: "DELETE",
},
expectedStatus: 204,
expectedError: nil,
},
{
name: "Get Policy After Delete - Invalid",
args: args{
api: "/policy?name=testdelete",
method: "GET",
},
expectedStatus: 500,
expectedError: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := &http.Client{
Timeout: 3 * time.Second,
}
request, err := http.NewRequest(
tt.args.method, fmt.Sprintf("http://localhost:9090/api/v1%s", tt.args.api), nil)
if err != nil {
log.Println(err)
return
}
request.Header.Add("Cookie", fmt.Sprintf("token=%s", token))
request.Header.Add("Content-Type", "application/json")
response, err := client.Do(request)
if err != nil {
log.Println(err)
return
}
if response != nil {
assert.Equal(tt.expectedStatus, response.StatusCode, tt.name+" Failed")
}
})
}
}

View File

@@ -0,0 +1,121 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package integration
import (
"archive/zip"
"bytes"
"fmt"
"io"
"log"
"net/http"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestStartProfiling(t *testing.T) {
testAsser := assert.New(t)
tests := []struct {
name string
}{
{
name: "start/stop profiling",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
files := map[string]bool{
"profile-127.0.0.1:9000-goroutines.txt": false,
"profile-127.0.0.1:9000-goroutines-before.txt": false,
"profile-127.0.0.1:9000-goroutines-before,debug=2.txt": false,
"profile-127.0.0.1:9000-threads-before.pprof": false,
"profile-127.0.0.1:9000-mem.pprof": false,
"profile-127.0.0.1:9000-threads.pprof": false,
"profile-127.0.0.1:9000-cpu.pprof": false,
"profile-127.0.0.1:9000-mem-before.pprof": false,
"profile-127.0.0.1:9000-block.pprof": false,
"profile-127.0.0.1:9000-trace.trace": false,
"profile-127.0.0.1:9000-mutex.pprof": false,
"profile-127.0.0.1:9000-mutex-before.pprof": false,
}
client := &http.Client{
Timeout: 3 * time.Second,
}
destination := "/api/v1/profiling/start"
finalURL := fmt.Sprintf("http://localhost:9090%s", destination)
request, err := http.NewRequest("POST", finalURL, strings.NewReader("{\"type\":\"cpu,mem,block,mutex,trace,threads,goroutines\"}"))
if err != nil {
log.Println(err)
return
}
request.Header.Add("Cookie", fmt.Sprintf("token=%s", token))
request.Header.Add("Content-Type", "application/json")
response, err := client.Do(request)
testAsser.Nil(err, fmt.Sprintf("%s returned an error: %v", tt.name, err))
testAsser.Equal(201, response.StatusCode)
time.Sleep(5 * time.Second)
destination = "/api/v1/profiling/stop"
finalURL = fmt.Sprintf("http://localhost:9090%s", destination)
request, err = http.NewRequest("POST", finalURL, nil)
if err != nil {
log.Println(err)
return
}
request.Header.Add("Cookie", fmt.Sprintf("token=%s", token))
request.Header.Add("Content-Type", "application/json")
response, err = client.Do(request)
testAsser.Nil(err, fmt.Sprintf("%s returned an error: %v", tt.name, err))
testAsser.Equal(200, response.StatusCode)
zipFileBytes, err := io.ReadAll(response.Body)
if err != nil {
testAsser.Nil(err, fmt.Sprintf("%s returned an error: %v", tt.name, err))
}
filetype := http.DetectContentType(zipFileBytes)
testAsser.Equal("application/zip", filetype)
zipReader, err := zip.NewReader(bytes.NewReader(zipFileBytes), int64(len(zipFileBytes)))
if err != nil {
testAsser.Nil(err, fmt.Sprintf("%s returned an error: %v", tt.name, err))
}
// Read all the files from zip archive
for _, zipFile := range zipReader.File {
files[zipFile.Name] = true
}
for k, v := range files {
testAsser.Equal(true, v, fmt.Sprintf("%s : compressed file expected to have %v file inside", tt.name, k))
}
})
}
}

View File

@@ -15,7 +15,7 @@ spec:
serviceAccountName: console-sa
containers:
- name: console
image: 'minio/console:v0.16.0'
image: 'minio/console:v0.16.1'
imagePullPolicy: "IfNotPresent"
env:
- name: CONSOLE_OPERATOR_MODE

View File

@@ -32,7 +32,7 @@ spec:
spec:
containers:
- name: console
image: 'minio/console:v0.16.0'
image: 'minio/console:v0.16.1'
imagePullPolicy: "IfNotPresent"
env:
- name: CONSOLE_MINIO_SERVER

View File

@@ -18,15 +18,14 @@ package auth
import (
"context"
"errors"
errors "github.com/minio/console/restapi"
"github.com/minio/console/cluster"
"github.com/minio/minio-go/v7/pkg/credentials"
operatorClientset "github.com/minio/operator/pkg/client/clientset/versioned"
)
var errInvalidCredentials = errors.New("invalid Login")
// operatorCredentialsProvider is an struct to hold the JWT (service account token)
type operatorCredentialsProvider struct {
serviceAccountJWT string
@@ -86,7 +85,7 @@ func GetConsoleCredentialsForOperator(jwt string) (*credentials.Credentials, err
client: opClientClientSet,
}
if err = checkServiceAccountTokenValid(ctx, opClient); err != nil {
return nil, errInvalidCredentials
return nil, errors.ErrInvalidLogin
}
return credentials.New(operatorCredentialsProvider{serviceAccountJWT: jwt}), nil
}

View File

@@ -23,7 +23,6 @@ import (
"strings"
"github.com/klauspost/compress/gzhttp"
"github.com/minio/console/restapi"
"github.com/unrolled/secure"
@@ -58,7 +57,7 @@ func configureAPI(api *operations.OperatorAPI) http.Handler {
api.KeyAuth = func(token string, scopes []string) (*models.Principal, error) {
// we are validating the session token by decrypting the claims inside, if the operation succeed that means the jwt
// was generated and signed by us in the first place
claims, err := auth.SessionTokenAuthenticate(token)
claims, err := auth.ParseClaimsFromToken(token)
if err != nil {
api.Logger("Unable to validate the session token %s: %v", token, err)
return nil, errors.New(401, "incorrect api key auth")
@@ -101,8 +100,8 @@ func configureAPI(api *operations.OperatorAPI) http.Handler {
// The TLS configuration before HTTPS server starts.
func configureTLS(tlsConfig *tls.Config) {
tlsConfig.RootCAs = GlobalRootCAs
tlsConfig.GetCertificate = GlobalTLSCertsManager.GetCertificate
tlsConfig.RootCAs = restapi.GlobalRootCAs
tlsConfig.GetCertificate = restapi.GlobalTLSCertsManager.GetCertificate
}
// As soon as server is initialized but not run yet, this function will be called.
@@ -118,24 +117,6 @@ func setupMiddlewares(handler http.Handler) http.Handler {
return handler
}
func AuthenticationMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token, err := auth.GetTokenFromRequest(r)
if err != nil && err != auth.ErrNoAuthToken {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
// All handlers handle appropriately to return errors
// based on their swagger rules, we do not need to
// additionally return error here, let the next ServeHTTPs
// handle it appropriately.
if token != "" {
r.Header.Add("Authorization", "Bearer "+token)
}
next.ServeHTTP(w, r)
})
}
// proxyMiddleware adds the proxy capability
func proxyMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -150,19 +131,23 @@ func proxyMiddleware(next http.Handler) http.Handler {
// The middleware configuration happens before anything, this middleware also applies to serving the swagger.json document.
// So this is a good place to plug in a panic handling middleware, logging and metrics.
func setupGlobalMiddleware(handler http.Handler) http.Handler {
// handle cookie or authorization header for session
next := AuthenticationMiddleware(handler)
// proxy requests
next = proxyMiddleware(next)
next := proxyMiddleware(handler)
// if audit-log is enabled console will log all incoming request
next = restapi.AuditLogMiddleware(next)
// serve static files
next = restapi.FileServerMiddleware(next)
// add information to request context
next = restapi.ContextMiddleware(next)
// handle cookie or authorization header for session
next = restapi.AuthenticationMiddleware(next)
// Secure middleware, this middleware wrap all the previous handlers and add
// HTTP security headers
secureOptions := secure.Options{
AllowedHosts: restapi.GetSecureAllowedHosts(),
AllowedHostsAreRegex: restapi.GetSecureAllowedHostsAreRegex(),
HostsProxyHeaders: restapi.GetSecureHostsProxyHeaders(),
SSLRedirect: restapi.GetTLSRedirect() == "on" && len(GlobalPublicCerts) > 0,
SSLRedirect: restapi.GetTLSRedirect() == "on" && len(restapi.GlobalPublicCerts) > 0,
SSLHost: restapi.GetSecureTLSHost(),
STSSeconds: restapi.GetSecureSTSSeconds(),
STSIncludeSubdomains: restapi.GetSecureSTSIncludeSubdomains(),

View File

@@ -208,7 +208,7 @@ func init() {
"get": {
"security": [],
"tags": [
"UserAPI"
"Auth"
],
"summary": "Returns login strategy, form or sso.",
"operationId": "LoginDetail",
@@ -232,7 +232,7 @@ func init() {
"post": {
"security": [],
"tags": [
"UserAPI"
"Auth"
],
"summary": "Identity Provider oauth2 callback endpoint.",
"operationId": "LoginOauth2Auth",
@@ -263,7 +263,7 @@ func init() {
"post": {
"security": [],
"tags": [
"UserAPI"
"Auth"
],
"summary": "Login to Operator Console.",
"operationId": "LoginOperator",
@@ -293,7 +293,7 @@ func init() {
"/logout": {
"post": {
"tags": [
"UserAPI"
"Auth"
],
"summary": "Logout from Operator.",
"operationId": "Logout",
@@ -1665,7 +1665,7 @@ func init() {
"/session": {
"get": {
"tags": [
"UserAPI"
"Auth"
],
"summary": "Endpoint to check if your session is still valid",
"operationId": "SessionCheck",
@@ -4278,7 +4278,7 @@ func init() {
"get": {
"security": [],
"tags": [
"UserAPI"
"Auth"
],
"summary": "Returns login strategy, form or sso.",
"operationId": "LoginDetail",
@@ -4302,7 +4302,7 @@ func init() {
"post": {
"security": [],
"tags": [
"UserAPI"
"Auth"
],
"summary": "Identity Provider oauth2 callback endpoint.",
"operationId": "LoginOauth2Auth",
@@ -4333,7 +4333,7 @@ func init() {
"post": {
"security": [],
"tags": [
"UserAPI"
"Auth"
],
"summary": "Login to Operator Console.",
"operationId": "LoginOperator",
@@ -4363,7 +4363,7 @@ func init() {
"/logout": {
"post": {
"tags": [
"UserAPI"
"Auth"
],
"summary": "Logout from Operator.",
"operationId": "Logout",
@@ -5735,7 +5735,7 @@ func init() {
"/session": {
"get": {
"tags": [
"UserAPI"
"Auth"
],
"summary": "Endpoint to check if your session is still valid",
"operationId": "SessionCheck",

View File

@@ -1,219 +0,0 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 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 operatorapi
import (
"errors"
"runtime"
"strings"
"github.com/go-openapi/swag"
"github.com/minio/console/models"
"github.com/minio/madmin-go"
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
)
var (
// Generic error messages
errorGeneric = errors.New("an error occurred, please try again")
errInvalidCredentials = errors.New("invalid Login")
errorGenericInvalidSession = errors.New("invalid session")
errorGenericUnauthorized = errors.New("unauthorized")
errorGenericForbidden = errors.New("forbidden")
// ErrorGenericNotFound Generic error for not found
ErrorGenericNotFound = errors.New("not found")
// Explicit error messages
errorInvalidErasureCodingValue = errors.New("invalid Erasure Coding Value")
errorUnableToGetTenantUsage = errors.New("unable to get tenant usage")
errorUnableToGetTenantLogs = errors.New("unable to get tenant logs")
errorUnableToUpdateTenantCertificates = errors.New("unable to update tenant certificates")
errorUpdatingEncryptionConfig = errors.New("unable to update encryption configuration")
errorDeletingEncryptionConfig = errors.New("error disabling tenant encryption")
errorEncryptionConfigNotFound = errors.New("encryption configuration not found")
errBucketBodyNotInRequest = errors.New("error bucket body not in request")
errBucketNameNotInRequest = errors.New("error bucket name not in request")
errGroupBodyNotInRequest = errors.New("error group body not in request")
errGroupNameNotInRequest = errors.New("error group name not in request")
errPolicyNameNotInRequest = errors.New("error policy name not in request")
errPolicyBodyNotInRequest = errors.New("error policy body not in request")
errSSENotConfigured = errors.New("error server side encryption configuration not found")
errBucketLifeCycleNotConfigured = errors.New("error bucket life cycle configuration not found")
errChangePassword = errors.New("error please check your current password")
errInvalidLicense = errors.New("invalid license key")
errLicenseNotFound = errors.New("license not found")
errAvoidSelfAccountDelete = errors.New("logged in user cannot be deleted by itself")
errAccessDenied = errors.New("access denied")
errTooManyNodes = errors.New("cannot request more nodes than what is available in the cluster")
errTooFewNodes = errors.New("there are not enough nodes in the cluster to support this tenant")
errTooFewSchedulableNodes = errors.New("there is not enough schedulable nodes to satisfy this requirement")
errFewerThanFourNodes = errors.New("at least 4 nodes are required for a tenant")
)
// prepareError receives an error object and parse it against k8sErrors, returns the right error code paired with a generic error message
func prepareError(err ...error) *models.Error {
errorCode := int32(500)
errorMessage := errorGeneric.Error()
if len(err) > 0 {
frame := getFrame(2)
fileParts := strings.Split(frame.File, "/")
LogError("original error -> (%s:%d: %v)", fileParts[len(fileParts)-1], frame.Line, err[0])
if k8sErrors.IsUnauthorized(err[0]) {
errorCode = 401
errorMessage = errorGenericUnauthorized.Error()
}
if k8sErrors.IsForbidden(err[0]) {
errorCode = 403
errorMessage = errorGenericForbidden.Error()
}
if k8sErrors.IsNotFound(err[0]) {
errorCode = 404
errorMessage = ErrorGenericNotFound.Error()
}
if err[0] == ErrorGenericNotFound {
errorCode = 404
errorMessage = ErrorGenericNotFound.Error()
}
if errors.Is(err[0], errInvalidCredentials) {
errorCode = 401
errorMessage = errInvalidCredentials.Error()
}
// console invalid erasure coding value
if errors.Is(err[0], errorInvalidErasureCodingValue) {
errorCode = 400
errorMessage = errorInvalidErasureCodingValue.Error()
}
if errors.Is(err[0], errBucketBodyNotInRequest) {
errorCode = 400
errorMessage = errBucketBodyNotInRequest.Error()
}
if errors.Is(err[0], errBucketNameNotInRequest) {
errorCode = 400
errorMessage = errBucketNameNotInRequest.Error()
}
if errors.Is(err[0], errGroupBodyNotInRequest) {
errorCode = 400
errorMessage = errGroupBodyNotInRequest.Error()
}
if errors.Is(err[0], errGroupNameNotInRequest) {
errorCode = 400
errorMessage = errGroupNameNotInRequest.Error()
}
if errors.Is(err[0], errPolicyNameNotInRequest) {
errorCode = 400
errorMessage = errPolicyNameNotInRequest.Error()
}
if errors.Is(err[0], errPolicyBodyNotInRequest) {
errorCode = 400
errorMessage = errPolicyBodyNotInRequest.Error()
}
// console invalid session error
if errors.Is(err[0], errorGenericInvalidSession) {
errorCode = 401
errorMessage = errorGenericInvalidSession.Error()
}
// Bucket life cycle not configured
if errors.Is(err[0], errBucketLifeCycleNotConfigured) {
errorCode = 404
errorMessage = errBucketLifeCycleNotConfigured.Error()
}
// Encryption not configured
if errors.Is(err[0], errSSENotConfigured) {
errorCode = 404
errorMessage = errSSENotConfigured.Error()
}
// account change password
if madmin.ToErrorResponse(err[0]).Code == "SignatureDoesNotMatch" {
errorCode = 403
errorMessage = errChangePassword.Error()
}
if errors.Is(err[0], errLicenseNotFound) {
errorCode = 404
errorMessage = errLicenseNotFound.Error()
}
if errors.Is(err[0], errInvalidLicense) {
errorCode = 404
errorMessage = errInvalidLicense.Error()
}
if errors.Is(err[0], errAvoidSelfAccountDelete) {
errorCode = 403
errorMessage = errAvoidSelfAccountDelete.Error()
}
if madmin.ToErrorResponse(err[0]).Code == "AccessDenied" {
errorCode = 403
errorMessage = errAccessDenied.Error()
}
if madmin.ToErrorResponse(err[0]).Code == "InvalidAccessKeyId" {
errorCode = 401
errorMessage = errorGenericInvalidSession.Error()
}
// console invalid session error
if madmin.ToErrorResponse(err[0]).Code == "XMinioAdminNoSuchUser" {
errorCode = 401
errorMessage = errorGenericInvalidSession.Error()
}
// if we received a second error take that as friendly message but dont override the code
if len(err) > 1 && err[1] != nil {
LogError("friendly error: %v", err[1].Error())
errorMessage = err[1].Error()
}
// if we receive third error we just print that as debugging
if len(err) > 2 && err[2] != nil {
LogError("debugging error: %v", err[2].Error())
}
errRemoteTierExists := errors.New("Specified remote tier already exists") //nolint
if errors.Is(err[0], errRemoteTierExists) {
errorMessage = err[0].Error()
}
if errors.Is(err[0], errTooFewNodes) {
errorCode = 507
errorMessage = errTooFewNodes.Error()
}
if errors.Is(err[0], errTooFewSchedulableNodes) {
errorCode = 507
errorMessage = errTooFewSchedulableNodes.Error()
}
if errors.Is(err[0], errFewerThanFourNodes) {
errorCode = 507
errorMessage = errFewerThanFourNodes.Error()
}
}
return &models.Error{Code: errorCode, Message: swag.String(errorMessage), DetailedMessage: swag.String(err[0].Error())}
}
func getFrame(skipFrames int) runtime.Frame {
// We need the frame at index skipFrames+2, since we never want runtime.Callers and getFrame
targetFrameIndex := skipFrames + 2
// Set size to targetFrameIndex+2 to ensure we have room for one more caller than we need
programCounters := make([]uintptr, targetFrameIndex+2)
n := runtime.Callers(0, programCounters)
frame := runtime.Frame{Function: "unknown"}
if n > 0 {
frames := runtime.CallersFrames(programCounters[:n])
for more, frameIndex := true, 0; more && frameIndex <= targetFrameIndex; frameIndex++ {
var frameCandidate runtime.Frame
frameCandidate, more = frames.Next()
if frameIndex == targetFrameIndex {
frame = frameCandidate
}
}
}
return frame
}

View File

@@ -33,44 +33,45 @@ import (
"github.com/minio/console/models"
opauth "github.com/minio/console/operatorapi/auth"
"github.com/minio/console/operatorapi/operations"
"github.com/minio/console/operatorapi/operations/user_api"
authApi "github.com/minio/console/operatorapi/operations/auth"
"github.com/minio/console/pkg/auth"
"github.com/minio/console/pkg/auth/idp/oauth2"
)
func registerLoginHandlers(api *operations.OperatorAPI) {
// GET login strategy
api.UserAPILoginDetailHandler = user_api.LoginDetailHandlerFunc(func(params user_api.LoginDetailParams) middleware.Responder {
loginDetails, err := getLoginDetailsResponse(params.HTTPRequest)
api.AuthLoginDetailHandler = authApi.LoginDetailHandlerFunc(func(params authApi.LoginDetailParams) middleware.Responder {
loginDetails, err := getLoginDetailsResponse(params)
if err != nil {
return user_api.NewLoginDetailDefault(int(err.Code)).WithPayload(err)
return authApi.NewLoginDetailDefault(int(err.Code)).WithPayload(err)
}
return user_api.NewLoginDetailOK().WithPayload(loginDetails)
return authApi.NewLoginDetailOK().WithPayload(loginDetails)
})
// POST login using k8s service account token
api.UserAPILoginOperatorHandler = user_api.LoginOperatorHandlerFunc(func(params user_api.LoginOperatorParams) middleware.Responder {
loginResponse, err := getLoginOperatorResponse(params.Body)
api.AuthLoginOperatorHandler = authApi.LoginOperatorHandlerFunc(func(params authApi.LoginOperatorParams) middleware.Responder {
loginResponse, err := getLoginOperatorResponse(params)
if err != nil {
return user_api.NewLoginOperatorDefault(int(err.Code)).WithPayload(err)
return authApi.NewLoginOperatorDefault(int(err.Code)).WithPayload(err)
}
// Custom response writer to set the session cookies
return middleware.ResponderFunc(func(w http.ResponseWriter, p runtime.Producer) {
cookie := restapi.NewSessionCookieForConsole(loginResponse.SessionID)
http.SetCookie(w, &cookie)
user_api.NewLoginOperatorNoContent().WriteResponse(w, p)
authApi.NewLoginOperatorNoContent().WriteResponse(w, p)
})
})
// POST login using external IDP
api.UserAPILoginOauth2AuthHandler = user_api.LoginOauth2AuthHandlerFunc(func(params user_api.LoginOauth2AuthParams) middleware.Responder {
loginResponse, err := getLoginOauth2AuthResponse(params.HTTPRequest, params.Body)
api.AuthLoginOauth2AuthHandler = authApi.LoginOauth2AuthHandlerFunc(func(params authApi.LoginOauth2AuthParams) middleware.Responder {
loginResponse, err := getLoginOauth2AuthResponse(params)
if err != nil {
return user_api.NewLoginOauth2AuthDefault(int(err.Code)).WithPayload(err)
return authApi.NewLoginOauth2AuthDefault(int(err.Code)).WithPayload(err)
}
// Custom response writer to set the session cookies
return middleware.ResponderFunc(func(w http.ResponseWriter, p runtime.Producer) {
cookie := restapi.NewSessionCookieForConsole(loginResponse.SessionID)
http.SetCookie(w, &cookie)
user_api.NewLoginOauth2AuthNoContent().WriteResponse(w, p)
authApi.NewLoginOauth2AuthNoContent().WriteResponse(w, p)
})
})
}
@@ -86,14 +87,19 @@ func login(credentials restapi.ConsoleCredentialsI) (*string, error) {
// if we made it here, the consoleCredentials work, generate a jwt with claims
token, err := auth.NewEncryptedTokenForClient(&tokens, credentials.GetAccountAccessKey(), nil)
if err != nil {
LogError("error authenticating user: %v", err)
return nil, errInvalidCredentials
restapi.LogError("error authenticating user: %v", err)
return nil, restapi.ErrInvalidLogin
}
return &token, nil
}
// getLoginDetailsResponse returns information regarding the Console authentication mechanism.
func getLoginDetailsResponse(r *http.Request) (*models.LoginDetails, *models.Error) {
func getLoginDetailsResponse(params authApi.LoginDetailParams) (*models.LoginDetails, *models.Error) {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
r := params.HTTPRequest
loginStrategy := models.LoginDetailsLoginStrategyServiceDashAccount
redirectURL := ""
@@ -102,7 +108,7 @@ func getLoginDetailsResponse(r *http.Request) (*models.LoginDetails, *models.Err
// initialize new oauth2 client
oauth2Client, err := oauth2.NewOauth2ProviderClient(nil, r, restapi.GetConsoleHTTPClient())
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
// Validate user against IDP
identityProvider := &auth.IdentityProvider{Client: oauth2Client}
@@ -125,31 +131,35 @@ func verifyUserAgainstIDP(ctx context.Context, provider auth.IdentityProviderI,
return oauth2Token, nil
}
func getLoginOauth2AuthResponse(r *http.Request, lr *models.LoginOauth2AuthRequest) (*models.LoginResponse, *models.Error) {
ctx, cancel := context.WithCancel(context.Background())
func getLoginOauth2AuthResponse(params authApi.LoginOauth2AuthParams) (*models.LoginResponse, *models.Error) {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
r := params.HTTPRequest
lr := params.Body
if oauth2.IsIDPEnabled() {
// initialize new oauth2 client
oauth2Client, err := oauth2.NewOauth2ProviderClient(nil, r, restapi.GetConsoleHTTPClient())
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
// initialize new identity provider
identityProvider := auth.IdentityProvider{Client: oauth2Client}
// Validate user against IDP
_, err = verifyUserAgainstIDP(ctx, identityProvider, *lr.Code, *lr.State)
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
// If we pass here that means the IDP correctly authenticate the user with the operator resource
// we proceed to use the service account token configured in the operator-console pod
creds, err := newConsoleCredentials(getK8sSAToken())
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
token, err := login(restapi.ConsoleCredentials{ConsoleCredentials: creds})
if err != nil {
return nil, prepareError(errInvalidCredentials, nil, err)
return nil, restapi.ErrorWithContext(ctx, restapi.ErrInvalidLogin, nil, err)
}
// serialize output
loginResponse := &models.LoginResponse{
@@ -157,7 +167,7 @@ func getLoginOauth2AuthResponse(r *http.Request, lr *models.LoginOauth2AuthReque
}
return loginResponse, nil
}
return nil, prepareError(errorGeneric)
return nil, restapi.ErrorWithContext(ctx, restapi.ErrDefault)
}
func newConsoleCredentials(secretKey string) (*credentials.Credentials, error) {
@@ -169,17 +179,22 @@ func newConsoleCredentials(secretKey string) (*credentials.Credentials, error) {
}
// getLoginOperatorResponse validate the provided service account token against k8s api
func getLoginOperatorResponse(lmr *models.LoginOperatorRequest) (*models.LoginResponse, *models.Error) {
func getLoginOperatorResponse(params authApi.LoginOperatorParams) (*models.LoginResponse, *models.Error) {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
lmr := params.Body
creds, err := newConsoleCredentials(*lmr.Jwt)
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
consoleCreds := restapi.ConsoleCredentials{ConsoleCredentials: creds}
// Set a random as access key as session identifier
consoleCreds.AccountAccessKey = fmt.Sprintf("%d", rand.Intn(100000-10000)+10000)
token, err := login(consoleCreds)
if err != nil {
return nil, prepareError(errInvalidCredentials, nil, err)
return nil, restapi.ErrorWithContext(ctx, restapi.ErrInvalidLogin, nil, err)
}
// serialize output
loginResponse := &models.LoginResponse{

View File

@@ -23,20 +23,20 @@ import (
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/models"
"github.com/minio/console/operatorapi/operations"
"github.com/minio/console/operatorapi/operations/user_api"
authApi "github.com/minio/console/operatorapi/operations/auth"
"github.com/minio/console/restapi"
)
func registerLogoutHandlers(api *operations.OperatorAPI) {
// logout from console
api.UserAPILogoutHandler = user_api.LogoutHandlerFunc(func(params user_api.LogoutParams, session *models.Principal) middleware.Responder {
api.AuthLogoutHandler = authApi.LogoutHandlerFunc(func(params authApi.LogoutParams, session *models.Principal) middleware.Responder {
// Custom response writer to expire the session cookies
return middleware.ResponderFunc(func(w http.ResponseWriter, p runtime.Producer) {
expiredCookie := restapi.ExpireSessionCookie()
// this will tell the browser to clear the cookie and invalidate user session
// additionally we are deleting the cookie from the client side
http.SetCookie(w, &expiredCookie)
user_api.NewLogoutOK().WriteResponse(w, p)
authApi.NewLogoutOK().WriteResponse(w, p)
})
})
}

View File

@@ -20,12 +20,13 @@ import (
"context"
"errors"
"github.com/minio/console/operatorapi/operations/operator_api"
xerrors "github.com/minio/console/restapi"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/cluster"
"github.com/minio/console/models"
"github.com/minio/console/operatorapi/operations"
"github.com/minio/console/operatorapi/operations/operator_api"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
v1 "k8s.io/client-go/kubernetes/typed/core/v1"
@@ -44,12 +45,12 @@ func registerNamespaceHandlers(api *operations.OperatorAPI) {
}
func getNamespaceCreatedResponse(session *models.Principal, params operator_api.CreateNamespaceParams) *models.Error {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
clientset, err := cluster.K8sClient(session.STSSessionToken)
if err != nil {
return prepareError(err)
return xerrors.ErrorWithContext(ctx, err)
}
namespace := *params.Body.Name
@@ -57,7 +58,7 @@ func getNamespaceCreatedResponse(session *models.Principal, params operator_api.
errCreation := getNamespaceCreated(ctx, clientset.CoreV1(), namespace)
if errCreation != nil {
return prepareError(errCreation)
return xerrors.ErrorWithContext(ctx, errCreation)
}
return nil

View File

@@ -21,6 +21,8 @@ import (
"errors"
"sort"
xerrors "github.com/minio/console/restapi"
"github.com/minio/minio-go/v7/pkg/set"
"github.com/minio/console/operatorapi/operations/operator_api"
@@ -52,8 +54,8 @@ func registerNodesHandlers(api *operations.OperatorAPI) {
return operator_api.NewListNodeLabelsOK().WithPayload(*resp)
})
api.OperatorAPIGetAllocatableResourcesHandler = operator_api.GetAllocatableResourcesHandlerFunc(func(params operator_api.GetAllocatableResourcesParams, principal *models.Principal) middleware.Responder {
resp, err := getAllocatableResourcesResponse(params.NumNodes, principal)
api.OperatorAPIGetAllocatableResourcesHandler = operator_api.GetAllocatableResourcesHandlerFunc(func(params operator_api.GetAllocatableResourcesParams, session *models.Principal) middleware.Responder {
resp, err := getAllocatableResourcesResponse(session, params)
if err != nil {
return operator_api.NewGetAllocatableResourcesDefault(int(err.Code)).WithPayload(err)
}
@@ -71,7 +73,7 @@ type NodeResourceInfo struct {
func getMaxAllocatableMemory(ctx context.Context, clientset v1.CoreV1Interface, numNodes int32) (*models.MaxAllocatableMemResponse, error) {
// can't request less than 4 nodes
if numNodes < 4 {
return nil, errFewerThanFourNodes
return nil, xerrors.ErrFewerThanFourNodes
}
// get all nodes from cluster
@@ -97,15 +99,15 @@ func getMaxAllocatableMemory(ctx context.Context, clientset v1.CoreV1Interface,
}
// requesting more nodes than schedulable and less than total number of workers
if int(numNodes) > schedulableNodes && int(numNodes) < nonMasterNodes {
return nil, errTooManyNodes
return nil, xerrors.ErrTooManyNodes
}
if nonMasterNodes < int(numNodes) {
return nil, errTooFewNodes
return nil, xerrors.ErrTooFewNodes
}
// not enough schedulable nodes
if schedulableNodes < int(numNodes) {
return nil, errTooFewSchedulableNodes
return nil, xerrors.ErrTooFewAvailableNodes
}
availableMemSizes := []int64{}
@@ -177,12 +179,12 @@ func min(x, y int64) int64 {
func getMaxAllocatableMemoryResponse(ctx context.Context, session *models.Principal, numNodes int32) (*models.MaxAllocatableMemResponse, *models.Error) {
client, err := cluster.K8sClient(session.STSSessionToken)
if err != nil {
return nil, prepareError(err)
return nil, xerrors.ErrorWithContext(ctx, err)
}
clusterResources, err := getMaxAllocatableMemory(ctx, client.CoreV1(), numNodes)
if err != nil {
return nil, prepareError(err)
return nil, xerrors.ErrorWithContext(ctx, err)
}
return clusterResources, nil
}
@@ -217,12 +219,12 @@ func getNodeLabels(ctx context.Context, clientset v1.CoreV1Interface) (*models.N
func getNodeLabelsResponse(ctx context.Context, session *models.Principal) (*models.NodeLabels, *models.Error) {
client, err := cluster.K8sClient(session.STSSessionToken)
if err != nil {
return nil, prepareError(err)
return nil, xerrors.ErrorWithContext(ctx, err)
}
clusterResources, err := getNodeLabels(ctx, client.CoreV1())
if err != nil {
return nil, prepareError(err)
return nil, xerrors.ErrorWithContext(ctx, err)
}
return clusterResources, nil
}
@@ -357,17 +359,16 @@ OUTER:
// Get allocatable resources response
func getAllocatableResourcesResponse(numNodes int32, session *models.Principal) (*models.AllocatableResourcesResponse, *models.Error) {
ctx, cancel := context.WithCancel(context.Background())
func getAllocatableResourcesResponse(session *models.Principal, params operator_api.GetAllocatableResourcesParams) (*models.AllocatableResourcesResponse, *models.Error) {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
client, err := cluster.K8sClient(session.STSSessionToken)
if err != nil {
return nil, prepareError(err)
return nil, xerrors.ErrorWithContext(ctx, err)
}
clusterResources, err := getAllocatableResources(ctx, client.CoreV1(), numNodes)
clusterResources, err := getAllocatableResources(ctx, client.CoreV1(), params.NumNodes)
if err != nil {
return nil, prepareError(err)
return nil, xerrors.ErrorWithContext(ctx, err)
}
return clusterResources, nil
}

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package user_api
package auth
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
@@ -46,7 +46,7 @@ func NewLoginDetail(ctx *middleware.Context, handler LoginDetailHandler) *LoginD
return &LoginDetail{Context: ctx, Handler: handler}
}
/* LoginDetail swagger:route GET /login UserAPI loginDetail
/* LoginDetail swagger:route GET /login Auth loginDetail
Returns login strategy, form or sso.

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package user_api
package auth
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package user_api
package auth
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package user_api
package auth
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package user_api
package auth
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
@@ -46,7 +46,7 @@ func NewLoginOauth2Auth(ctx *middleware.Context, handler LoginOauth2AuthHandler)
return &LoginOauth2Auth{Context: ctx, Handler: handler}
}
/* LoginOauth2Auth swagger:route POST /login/oauth2/auth UserAPI loginOauth2Auth
/* LoginOauth2Auth swagger:route POST /login/oauth2/auth Auth loginOauth2Auth
Identity Provider oauth2 callback endpoint.

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package user_api
package auth
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package user_api
package auth
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package user_api
package auth
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package user_api
package auth
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
@@ -46,7 +46,7 @@ func NewLoginOperator(ctx *middleware.Context, handler LoginOperatorHandler) *Lo
return &LoginOperator{Context: ctx, Handler: handler}
}
/* LoginOperator swagger:route POST /login/operator UserAPI loginOperator
/* LoginOperator swagger:route POST /login/operator Auth loginOperator
Login to Operator Console.

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package user_api
package auth
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package user_api
package auth
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package user_api
package auth
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package user_api
package auth
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
@@ -48,7 +48,7 @@ func NewLogout(ctx *middleware.Context, handler LogoutHandler) *Logout {
return &Logout{Context: ctx, Handler: handler}
}
/* Logout swagger:route POST /logout UserAPI logout
/* Logout swagger:route POST /logout Auth logout
Logout from Operator.

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package user_api
package auth
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package user_api
package auth
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package user_api
package auth
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package user_api
package auth
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
@@ -48,7 +48,7 @@ func NewSessionCheck(ctx *middleware.Context, handler SessionCheckHandler) *Sess
return &SessionCheck{Context: ctx, Handler: handler}
}
/* SessionCheck swagger:route GET /session UserAPI sessionCheck
/* SessionCheck swagger:route GET /session Auth sessionCheck
Endpoint to check if your session is still valid

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package user_api
package auth
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package user_api
package auth
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package user_api
package auth
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command

View File

@@ -37,6 +37,7 @@ import (
"github.com/go-openapi/swag"
"github.com/minio/console/models"
"github.com/minio/console/operatorapi/operations/auth"
"github.com/minio/console/operatorapi/operations/operator_api"
"github.com/minio/console/operatorapi/operations/user_api"
)
@@ -141,23 +142,23 @@ func NewOperatorAPI(spec *loads.Document) *OperatorAPI {
OperatorAPIListTenantsHandler: operator_api.ListTenantsHandlerFunc(func(params operator_api.ListTenantsParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation operator_api.ListTenants has not yet been implemented")
}),
UserAPILoginDetailHandler: user_api.LoginDetailHandlerFunc(func(params user_api.LoginDetailParams) middleware.Responder {
return middleware.NotImplemented("operation user_api.LoginDetail has not yet been implemented")
AuthLoginDetailHandler: auth.LoginDetailHandlerFunc(func(params auth.LoginDetailParams) middleware.Responder {
return middleware.NotImplemented("operation auth.LoginDetail has not yet been implemented")
}),
UserAPILoginOauth2AuthHandler: user_api.LoginOauth2AuthHandlerFunc(func(params user_api.LoginOauth2AuthParams) middleware.Responder {
return middleware.NotImplemented("operation user_api.LoginOauth2Auth has not yet been implemented")
AuthLoginOauth2AuthHandler: auth.LoginOauth2AuthHandlerFunc(func(params auth.LoginOauth2AuthParams) middleware.Responder {
return middleware.NotImplemented("operation auth.LoginOauth2Auth has not yet been implemented")
}),
UserAPILoginOperatorHandler: user_api.LoginOperatorHandlerFunc(func(params user_api.LoginOperatorParams) middleware.Responder {
return middleware.NotImplemented("operation user_api.LoginOperator has not yet been implemented")
AuthLoginOperatorHandler: auth.LoginOperatorHandlerFunc(func(params auth.LoginOperatorParams) middleware.Responder {
return middleware.NotImplemented("operation auth.LoginOperator has not yet been implemented")
}),
UserAPILogoutHandler: user_api.LogoutHandlerFunc(func(params user_api.LogoutParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation user_api.Logout has not yet been implemented")
AuthLogoutHandler: auth.LogoutHandlerFunc(func(params auth.LogoutParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation auth.Logout has not yet been implemented")
}),
OperatorAPIPutTenantYAMLHandler: operator_api.PutTenantYAMLHandlerFunc(func(params operator_api.PutTenantYAMLParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation operator_api.PutTenantYAML has not yet been implemented")
}),
UserAPISessionCheckHandler: user_api.SessionCheckHandlerFunc(func(params user_api.SessionCheckParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation user_api.SessionCheck has not yet been implemented")
AuthSessionCheckHandler: auth.SessionCheckHandlerFunc(func(params auth.SessionCheckParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation auth.SessionCheck has not yet been implemented")
}),
OperatorAPISetTenantLogsHandler: operator_api.SetTenantLogsHandlerFunc(func(params operator_api.SetTenantLogsParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation operator_api.SetTenantLogs has not yet been implemented")
@@ -317,18 +318,18 @@ type OperatorAPI struct {
OperatorAPIListPVCsForTenantHandler operator_api.ListPVCsForTenantHandler
// OperatorAPIListTenantsHandler sets the operation handler for the list tenants operation
OperatorAPIListTenantsHandler operator_api.ListTenantsHandler
// UserAPILoginDetailHandler sets the operation handler for the login detail operation
UserAPILoginDetailHandler user_api.LoginDetailHandler
// UserAPILoginOauth2AuthHandler sets the operation handler for the login oauth2 auth operation
UserAPILoginOauth2AuthHandler user_api.LoginOauth2AuthHandler
// UserAPILoginOperatorHandler sets the operation handler for the login operator operation
UserAPILoginOperatorHandler user_api.LoginOperatorHandler
// UserAPILogoutHandler sets the operation handler for the logout operation
UserAPILogoutHandler user_api.LogoutHandler
// AuthLoginDetailHandler sets the operation handler for the login detail operation
AuthLoginDetailHandler auth.LoginDetailHandler
// AuthLoginOauth2AuthHandler sets the operation handler for the login oauth2 auth operation
AuthLoginOauth2AuthHandler auth.LoginOauth2AuthHandler
// AuthLoginOperatorHandler sets the operation handler for the login operator operation
AuthLoginOperatorHandler auth.LoginOperatorHandler
// AuthLogoutHandler sets the operation handler for the logout operation
AuthLogoutHandler auth.LogoutHandler
// OperatorAPIPutTenantYAMLHandler sets the operation handler for the put tenant y a m l operation
OperatorAPIPutTenantYAMLHandler operator_api.PutTenantYAMLHandler
// UserAPISessionCheckHandler sets the operation handler for the session check operation
UserAPISessionCheckHandler user_api.SessionCheckHandler
// AuthSessionCheckHandler sets the operation handler for the session check operation
AuthSessionCheckHandler auth.SessionCheckHandler
// OperatorAPISetTenantLogsHandler sets the operation handler for the set tenant logs operation
OperatorAPISetTenantLogsHandler operator_api.SetTenantLogsHandler
// OperatorAPISetTenantMonitoringHandler sets the operation handler for the set tenant monitoring operation
@@ -526,23 +527,23 @@ func (o *OperatorAPI) Validate() error {
if o.OperatorAPIListTenantsHandler == nil {
unregistered = append(unregistered, "operator_api.ListTenantsHandler")
}
if o.UserAPILoginDetailHandler == nil {
unregistered = append(unregistered, "user_api.LoginDetailHandler")
if o.AuthLoginDetailHandler == nil {
unregistered = append(unregistered, "auth.LoginDetailHandler")
}
if o.UserAPILoginOauth2AuthHandler == nil {
unregistered = append(unregistered, "user_api.LoginOauth2AuthHandler")
if o.AuthLoginOauth2AuthHandler == nil {
unregistered = append(unregistered, "auth.LoginOauth2AuthHandler")
}
if o.UserAPILoginOperatorHandler == nil {
unregistered = append(unregistered, "user_api.LoginOperatorHandler")
if o.AuthLoginOperatorHandler == nil {
unregistered = append(unregistered, "auth.LoginOperatorHandler")
}
if o.UserAPILogoutHandler == nil {
unregistered = append(unregistered, "user_api.LogoutHandler")
if o.AuthLogoutHandler == nil {
unregistered = append(unregistered, "auth.LogoutHandler")
}
if o.OperatorAPIPutTenantYAMLHandler == nil {
unregistered = append(unregistered, "operator_api.PutTenantYAMLHandler")
}
if o.UserAPISessionCheckHandler == nil {
unregistered = append(unregistered, "user_api.SessionCheckHandler")
if o.AuthSessionCheckHandler == nil {
unregistered = append(unregistered, "auth.SessionCheckHandler")
}
if o.OperatorAPISetTenantLogsHandler == nil {
unregistered = append(unregistered, "operator_api.SetTenantLogsHandler")
@@ -806,19 +807,19 @@ func (o *OperatorAPI) initHandlerCache() {
if o.handlers["GET"] == nil {
o.handlers["GET"] = make(map[string]http.Handler)
}
o.handlers["GET"]["/login"] = user_api.NewLoginDetail(o.context, o.UserAPILoginDetailHandler)
o.handlers["GET"]["/login"] = auth.NewLoginDetail(o.context, o.AuthLoginDetailHandler)
if o.handlers["POST"] == nil {
o.handlers["POST"] = make(map[string]http.Handler)
}
o.handlers["POST"]["/login/oauth2/auth"] = user_api.NewLoginOauth2Auth(o.context, o.UserAPILoginOauth2AuthHandler)
o.handlers["POST"]["/login/oauth2/auth"] = auth.NewLoginOauth2Auth(o.context, o.AuthLoginOauth2AuthHandler)
if o.handlers["POST"] == nil {
o.handlers["POST"] = make(map[string]http.Handler)
}
o.handlers["POST"]["/login/operator"] = user_api.NewLoginOperator(o.context, o.UserAPILoginOperatorHandler)
o.handlers["POST"]["/login/operator"] = auth.NewLoginOperator(o.context, o.AuthLoginOperatorHandler)
if o.handlers["POST"] == nil {
o.handlers["POST"] = make(map[string]http.Handler)
}
o.handlers["POST"]["/logout"] = user_api.NewLogout(o.context, o.UserAPILogoutHandler)
o.handlers["POST"]["/logout"] = auth.NewLogout(o.context, o.AuthLogoutHandler)
if o.handlers["PUT"] == nil {
o.handlers["PUT"] = make(map[string]http.Handler)
}
@@ -826,7 +827,7 @@ func (o *OperatorAPI) initHandlerCache() {
if o.handlers["GET"] == nil {
o.handlers["GET"] = make(map[string]http.Handler)
}
o.handlers["GET"]["/session"] = user_api.NewSessionCheck(o.context, o.UserAPISessionCheckHandler)
o.handlers["GET"]["/session"] = auth.NewSessionCheck(o.context, o.AuthSessionCheckHandler)
if o.handlers["PUT"] == nil {
o.handlers["PUT"] = make(map[string]http.Handler)
}

View File

@@ -17,9 +17,10 @@
package operatorapi
import (
"context"
"fmt"
"github.com/minio/console/restapi"
errors "github.com/minio/console/restapi"
"github.com/minio/console/pkg/utils"
@@ -50,14 +51,14 @@ func GetParityInfo(nodes int64, disksPerNode int64) (models.ParityResponse, erro
}
func getParityResponse(params operator_api.GetParityParams) (models.ParityResponse, *models.Error) {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
nodes := params.Nodes
disksPerNode := params.DisksPerNode
parityValues, err := GetParityInfo(nodes, disksPerNode)
if err != nil {
restapi.LogError("error getting parity info: %v", err)
return nil, prepareError(err)
errors.LogError("error getting parity info: %v", err)
return nil, errors.ErrorWithContext(ctx, err)
}
return parityValues, nil
}

View File

@@ -20,6 +20,8 @@ import (
"context"
"fmt"
xerrors "github.com/minio/console/restapi"
"k8s.io/apimachinery/pkg/api/errors"
"github.com/minio/console/cluster"
@@ -94,18 +96,18 @@ func getResourceQuota(ctx context.Context, client K8sClientI, namespace, resourc
}
func getResourceQuotaResponse(session *models.Principal, params operator_api.GetResourceQuotaParams) (*models.ResourceQuota, *models.Error) {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
client, err := cluster.K8sClient(session.STSSessionToken)
if err != nil {
return nil, prepareError(err)
return nil, xerrors.ErrorWithContext(ctx, err)
}
k8sClient := &k8sClient{
client: client,
}
resourceQuota, err := getResourceQuota(ctx, k8sClient, params.Namespace, params.ResourceQuotaName)
if err != nil {
return nil, prepareError(err)
return nil, xerrors.ErrorWithContext(ctx, err)
}
return resourceQuota, nil
}

View File

@@ -17,30 +17,35 @@
package operatorapi
import (
"context"
"fmt"
errors "github.com/minio/console/restapi"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/models"
"github.com/minio/console/operatorapi/operations"
"github.com/minio/console/operatorapi/operations/user_api"
authApi "github.com/minio/console/operatorapi/operations/auth"
)
func registerSessionHandlers(api *operations.OperatorAPI) {
// session check
api.UserAPISessionCheckHandler = user_api.SessionCheckHandlerFunc(func(params user_api.SessionCheckParams, session *models.Principal) middleware.Responder {
sessionResp, err := getSessionResponse(session)
api.AuthSessionCheckHandler = authApi.SessionCheckHandlerFunc(func(params authApi.SessionCheckParams, session *models.Principal) middleware.Responder {
sessionResp, err := getSessionResponse(session, params)
if err != nil {
return user_api.NewSessionCheckDefault(int(err.Code)).WithPayload(err)
return authApi.NewSessionCheckDefault(int(err.Code)).WithPayload(err)
}
return user_api.NewSessionCheckOK().WithPayload(sessionResp)
return authApi.NewSessionCheckOK().WithPayload(sessionResp)
})
}
// getSessionResponse parse the token of the current session and returns a list of allowed actions to render in the UI
func getSessionResponse(session *models.Principal) (*models.OperatorSessionResponse, *models.Error) {
func getSessionResponse(session *models.Principal, params authApi.SessionCheckParams) (*models.OperatorSessionResponse, *models.Error) {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
// serialize output
if session == nil {
return nil, prepareError(errorGenericInvalidSession)
return nil, errors.ErrorWithContext(ctx, errors.ErrInvalidSession)
}
sessionResp := &models.OperatorSessionResponse{
Status: models.OperatorSessionResponseStatusOk,

View File

@@ -41,7 +41,7 @@ import (
func getTenantCreatedResponse(session *models.Principal, params operator_api.CreateTenantParams) (response *models.CreateTenantResponse, mError *models.Error) {
tenantReq := params.Body
minioImage := tenantReq.Image
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
if minioImage == "" {
minImg, err := cluster.GetMinioImage()
@@ -56,7 +56,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
client: clientSet,
}
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
ns := *tenantReq.Namespace
@@ -98,7 +98,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
_, err = clientSet.CoreV1().Secrets(ns).Create(ctx, &instanceSecret, metav1.CreateOptions{})
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
// Enable/Disable console object browser for MinIO tenant (default is on)
@@ -110,7 +110,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
tenantConfigurationENV["MINIO_ROOT_USER"] = accessKey
tenantConfigurationENV["MINIO_ROOT_PASSWORD"] = secretKey
// delete secrets created if an error occurred during tenant creation,
// delete secrets created if an errors occurred during tenant creation,
defer func() {
if mError != nil {
restapi.LogError("deleting secrets created for failed tenant: %s if any: %v", tenantName, mError)
@@ -127,7 +127,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
// Check the Erasure Coding Parity for validity and pass it to Tenant
if tenantReq.ErasureCodingParity > 0 {
if tenantReq.ErasureCodingParity < 2 || tenantReq.ErasureCodingParity > 8 {
return nil, prepareError(errorInvalidErasureCodingValue)
return nil, restapi.ErrorWithContext(ctx, restapi.ErrInvalidErasureCodingValue)
}
tenantConfigurationENV["MINIO_STORAGE_CLASS_STANDARD"] = fmt.Sprintf("EC:%d", tenantReq.ErasureCodingParity)
}
@@ -204,7 +204,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
}
_, err := clientSet.CoreV1().Secrets(ns).Create(ctx, &userSecret, metav1.CreateOptions{})
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
}
// attach the users to the tenant
@@ -248,7 +248,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
}
_, err := clientSet.CoreV1().Secrets(ns).Create(ctx, &userSecret, metav1.CreateOptions{})
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
}
// attach the users to the tenant
@@ -274,7 +274,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
externalCertSecretName := fmt.Sprintf("%s-instance-external-certificates", secretName)
externalCertSecret, err := createOrReplaceExternalCertSecrets(ctx, &k8sClient, ns, tenantReq.TLS.Minio, externalCertSecretName, tenantName)
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
minInst.Spec.ExternalCertSecret = externalCertSecret
}
@@ -286,7 +286,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
certificates := []*models.KeyPairConfiguration{tenantReq.Encryption.Client}
certificateSecrets, err := createOrReplaceExternalCertSecrets(ctx, &k8sClient, ns, certificates, tenantExternalClientCertSecretName, tenantName)
if err != nil {
return nil, prepareError(restapi.ErrorGeneric)
return nil, restapi.ErrorWithContext(ctx, restapi.ErrDefault)
}
if len(certificateSecrets) > 0 {
minInst.Spec.ExternalClientCertSecret = certificateSecrets[0]
@@ -296,7 +296,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
// KES configuration for Tenant instance
minInst.Spec.KES, err = getKESConfiguration(ctx, &k8sClient, ns, tenantReq.Encryption, secretName, tenantName)
if err != nil {
return nil, prepareError(restapi.ErrorGeneric)
return nil, restapi.ErrorWithContext(ctx, restapi.ErrDefault)
}
// Set Labels, Annotations and Node Selector for KES
minInst.Spec.KES.Labels = tenantReq.Encryption.Labels
@@ -306,7 +306,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
if tenantReq.Encryption.SecurityContext != nil {
sc, err := convertModelSCToK8sSC(tenantReq.Encryption.SecurityContext)
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
minInst.Spec.KES.SecurityContext = sc
}
@@ -317,7 +317,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
for i, caCertificate := range tenantReq.TLS.CaCertificates {
certificateContent, err := base64.StdEncoding.DecodeString(caCertificate)
if err != nil {
return nil, prepareError(restapi.ErrorGeneric, nil, err)
return nil, restapi.ErrorWithContext(ctx, restapi.ErrDefault, nil, err)
}
caCertificates = append(caCertificates, tenantSecret{
Name: fmt.Sprintf("ca-certificate-%d", i),
@@ -329,7 +329,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
if len(caCertificates) > 0 {
certificateSecrets, err := createOrReplaceSecrets(ctx, &k8sClient, ns, caCertificates, tenantName)
if err != nil {
return nil, prepareError(restapi.ErrorGeneric, nil, err)
return nil, restapi.ErrorWithContext(ctx, restapi.ErrDefault, nil, err)
}
minInst.Spec.ExternalCaCertSecret = certificateSecrets
}
@@ -347,7 +347,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
pool, err := parseTenantPoolRequest(pool)
if err != nil {
restapi.LogError("parseTenantPoolRequest failed: %v", err)
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
minInst.Spec.Pools = append(minInst.Spec.Pools, *pool)
}
@@ -363,7 +363,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
if tenantReq.ImagePullSecret != "" {
imagePullSecret = tenantReq.ImagePullSecret
} else if imagePullSecret, err = setImageRegistry(ctx, tenantReq.ImageRegistry, clientSet.CoreV1(), ns, tenantName); err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
// pass the image pull secret to the Tenant
if imagePullSecret != "" {
@@ -410,7 +410,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
if tenantReq.LogSearchConfiguration.SecurityContext != nil {
sc, err := convertModelSCToK8sSC(tenantReq.LogSearchConfiguration.SecurityContext)
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
logSearchSecurityContext = sc
}
@@ -418,7 +418,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
if tenantReq.LogSearchConfiguration.PostgresSecurityContext != nil {
sc, err := convertModelSCToK8sSC(tenantReq.LogSearchConfiguration.PostgresSecurityContext)
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
logSearchPgSecurityContext = sc
}
@@ -514,7 +514,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
if tenantReq.PrometheusConfiguration != nil && tenantReq.PrometheusConfiguration.SecurityContext != nil {
sc, err := convertModelSCToK8sSC(tenantReq.PrometheusConfiguration.SecurityContext)
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
minInst.Spec.Prometheus.SecurityContext = sc
}
@@ -538,7 +538,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
},
}, tenantName)
if err != nil {
return nil, prepareError(restapi.ErrorGeneric, nil, err)
return nil, restapi.ErrorWithContext(ctx, restapi.ErrDefault, nil, err)
}
minInst.Spec.Configuration = &corev1.LocalObjectReference{Name: tenantConfigurationName}
@@ -562,20 +562,20 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
opClient, err := cluster.OperatorClient(session.STSSessionToken)
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
_, err = opClient.MinioV2().Tenants(ns).Create(context.Background(), &minInst, metav1.CreateOptions{})
if err != nil {
restapi.LogError("Creating new tenant failed with: %v", err)
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
// Integrations
if os.Getenv("GKE_INTEGRATION") != "" {
err := gkeIntegration(clientSet, tenantName, ns, session.STSSessionToken)
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
}
response = &models.CreateTenantResponse{

View File

@@ -20,20 +20,21 @@ import (
"context"
"fmt"
errors "github.com/minio/console/restapi"
"github.com/minio/console/cluster"
"github.com/minio/console/models"
"github.com/minio/console/operatorapi/operations/operator_api"
"github.com/minio/console/restapi"
miniov2 "github.com/minio/operator/pkg/apis/minio.min.io/v2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func getTenantDetailsResponse(session *models.Principal, params operator_api.TenantDetailsParams) (*models.Tenant, *models.Error) {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
if err != nil {
return nil, prepareError(err)
return nil, errors.ErrorWithContext(ctx, err)
}
opClient := &operatorClient{
@@ -42,7 +43,7 @@ func getTenantDetailsResponse(session *models.Principal, params operator_api.Ten
minTenant, err := getTenant(ctx, opClient, params.Namespace, params.Tenant)
if err != nil {
return nil, prepareError(err)
return nil, errors.ErrorWithContext(ctx, err)
}
info := getTenantInfo(minTenant)
@@ -50,7 +51,7 @@ func getTenantDetailsResponse(session *models.Principal, params operator_api.Ten
// get Kubernetes Client
clientSet, err := cluster.K8sClient(session.STSSessionToken)
if err != nil {
return nil, prepareError(err)
return nil, errors.ErrorWithContext(ctx, err)
}
k8sClient := k8sClient{
@@ -59,7 +60,7 @@ func getTenantDetailsResponse(session *models.Principal, params operator_api.Ten
tenantConfiguration, err := GetTenantConfiguration(ctx, &k8sClient, minTenant)
if err != nil {
restapi.LogError("unable to fetch configuration for tenant %s: %v", minTenant.Name, err)
errors.LogError("unable to fetch configuration for tenant %s: %v", minTenant.Name, err)
}
// detect if AD/LDAP is enabled
@@ -105,14 +106,14 @@ func getTenantDetailsResponse(session *models.Principal, params operator_api.Ten
//minio service
minSvc, err := k8sClient.getService(ctx, minTenant.Namespace, minTenant.MinIOCIServiceName(), metav1.GetOptions{})
if err != nil {
// we can tolerate this error
restapi.LogError("Unable to get MinIO service name: %v, continuing", err)
// we can tolerate this errors
errors.LogError("Unable to get MinIO service name: %v, continuing", err)
}
//console service
conSvc, err := k8sClient.getService(ctx, minTenant.Namespace, minTenant.ConsoleCIServiceName(), metav1.GetOptions{})
if err != nil {
// we can tolerate this error
restapi.LogError("Unable to get MinIO console service name: %v, continuing", err)
// we can tolerate this errors
errors.LogError("Unable to get MinIO console service name: %v, continuing", err)
}
schema := "http"

View File

@@ -22,9 +22,12 @@ import (
"fmt"
"strings"
"github.com/minio/console/restapi"
"github.com/minio/console/pkg/http"
"github.com/minio/console/operatorapi/operations/operator_api"
utils2 "github.com/minio/console/pkg/utils"
"github.com/minio/console/restapi"
miniov2 "github.com/minio/operator/pkg/apis/minio.min.io/v2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
@@ -32,7 +35,7 @@ import (
)
// updateTenantAction does an update on the minioTenant by patching the desired changes
func updateTenantAction(ctx context.Context, operatorClient OperatorClientI, clientset v1.CoreV1Interface, httpCl utils2.HTTPClientI, namespace string, params operator_api.UpdateTenantParams) error {
func updateTenantAction(ctx context.Context, operatorClient OperatorClientI, clientset v1.CoreV1Interface, httpCl http.ClientI, namespace string, params operator_api.UpdateTenantParams) error {
imageToUpdate := params.Body.Image
imageRegistryReq := params.Body.ImageRegistry

View File

@@ -33,11 +33,11 @@ import (
"strings"
"time"
utils2 "github.com/minio/console/pkg/http"
"github.com/dustin/go-humanize"
"github.com/minio/madmin-go"
utils2 "github.com/minio/console/pkg/utils"
"github.com/minio/console/restapi"
"github.com/minio/console/operatorapi/operations/operator_api"
@@ -354,14 +354,16 @@ func registerTenantHandlers(api *operations.OperatorAPI) {
// getDeleteTenantResponse gets the output of deleting a minio instance
func getDeleteTenantResponse(session *models.Principal, params operator_api.DeleteTenantParams) *models.Error {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
if err != nil {
return prepareError(err)
return restapi.ErrorWithContext(ctx, err)
}
// get Kubernetes Client
clientset, err := cluster.K8sClient(session.STSSessionToken)
if err != nil {
return prepareError(err)
return restapi.ErrorWithContext(ctx, err)
}
opClient := &operatorClient{
client: opClientClientSet,
@@ -373,12 +375,12 @@ func getDeleteTenantResponse(session *models.Principal, params operator_api.Dele
tenant, err := opClient.TenantGet(params.HTTPRequest.Context(), params.Namespace, params.Tenant, metav1.GetOptions{})
if err != nil {
return prepareError(err)
return restapi.ErrorWithContext(ctx, err)
}
tenant.EnsureDefaults()
if err = deleteTenantAction(params.HTTPRequest.Context(), opClient, clientset.CoreV1(), tenant, deleteTenantPVCs); err != nil {
return prepareError(err)
return restapi.ErrorWithContext(ctx, err)
}
return nil
}
@@ -396,7 +398,7 @@ func deleteTenantAction(
err := operatorClient.TenantDelete(ctx, tenant.Namespace, tenant.Name, metav1.DeleteOptions{})
if err != nil {
// try to delete pvc even if the tenant doesn't exist anymore but only if deletePvcs is set to true,
// else, we return the error
// else, we return the errors
if (deletePvcs && !k8sErrors.IsNotFound(err)) || !deletePvcs {
return err
}
@@ -440,19 +442,19 @@ func deleteTenantAction(
// getDeleteTenantResponse gets the output of deleting a minio instance
func getDeletePodResponse(session *models.Principal, params operator_api.DeletePodParams) *models.Error {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
// get Kubernetes Client
clientset, err := cluster.K8sClient(session.STSSessionToken)
if err != nil {
return prepareError(err)
return restapi.ErrorWithContext(ctx, err)
}
listOpts := metav1.ListOptions{
LabelSelector: fmt.Sprintf("v1.min.io/tenant=%s", params.Tenant),
FieldSelector: fmt.Sprintf("metadata.name=%s%s", params.Tenant, params.PodName[len(params.Tenant):]),
}
if err = clientset.CoreV1().Pods(params.Namespace).DeleteCollection(ctx, metav1.DeleteOptions{}, listOpts); err != nil {
return prepareError(err)
return restapi.ErrorWithContext(ctx, err)
}
return nil
}
@@ -494,12 +496,12 @@ func getTenantCreds(ctx context.Context, client K8sClientI, tenant *miniov2.Tena
tenantAccessKey, ok := tenantConfiguration["accesskey"]
if !ok {
restapi.LogError("tenant's secret doesn't contain accesskey")
return nil, restapi.ErrorGeneric
return nil, restapi.ErrDefault
}
tenantSecretKey, ok := tenantConfiguration["secretkey"]
if !ok {
restapi.LogError("tenant's secret doesn't contain secretkey")
return nil, restapi.ErrorGeneric
return nil, restapi.ErrDefault
}
return &tenantKeys{accessKey: tenantAccessKey, secretKey: tenantSecretKey}, nil
}
@@ -584,6 +586,13 @@ func parseCertificate(name string, rawCert []byte) (*models.CertificateInfo, err
}, nil
}
var secretTypePublicKeyNameMap = map[string]string{
"kubernetes.io/tls": "tls.crt",
"cert-manager.io/v1": "tls.crt",
"cert-manager.io/v1alpha2": "tls.crt",
// Add newer secretTypes and their corresponding values in future
}
// parseTenantCertificates convert public key pem certificates stored in k8s secrets for a given Tenant into x509 certificates
func parseTenantCertificates(ctx context.Context, clientSet K8sClientI, namespace string, secrets []*miniov2.LocalCertificateReference) ([]*models.CertificateInfo, error) {
var certificates []*models.CertificateInfo
@@ -595,9 +604,11 @@ func parseTenantCertificates(ctx context.Context, clientSet K8sClientI, namespac
if err != nil {
return nil, err
}
if secret.Type == "kubernetes.io/tls" || secret.Type == "cert-manager.io/v1alpha2" {
publicKey = "tls.crt"
if v, ok := secretTypePublicKeyNameMap[secret.Type]; ok {
publicKey = v
}
// Extract public key from certificate TLS secret
if rawCert, ok := keyPair.Data[publicKey]; ok {
var blocks []byte
@@ -842,19 +853,19 @@ func updateTenantIdentityProvider(ctx context.Context, operatorClient OperatorCl
func getTenantIdentityProviderResponse(session *models.Principal, params operator_api.TenantIdentityProviderParams) (*models.IdpConfiguration, *models.Error) {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
opClient := &operatorClient{
client: opClientClientSet,
}
minTenant, err := getTenant(ctx, opClient, params.Namespace, params.Tenant)
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
// get Kubernetes Client
clientSet, err := cluster.K8sClient(session.STSSessionToken)
@@ -862,27 +873,27 @@ func getTenantIdentityProviderResponse(session *models.Principal, params operato
client: clientSet,
}
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
info, err := getTenantIdentityProvider(ctx, &k8sClient, minTenant)
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
return info, nil
}
func getUpdateTenantIdentityProviderResponse(session *models.Principal, params operator_api.UpdateTenantIdentityProviderParams) *models.Error {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
if err != nil {
return prepareError(err)
return restapi.ErrorWithContext(ctx, err)
}
// get Kubernetes Client
clientSet, err := cluster.K8sClient(session.STSSessionToken)
if err != nil {
return prepareError(err)
return restapi.ErrorWithContext(ctx, err)
}
k8sClient := k8sClient{
client: clientSet,
@@ -891,25 +902,25 @@ func getUpdateTenantIdentityProviderResponse(session *models.Principal, params o
client: opClientClientSet,
}
if err := updateTenantIdentityProvider(ctx, opClient, &k8sClient, params.Namespace, params); err != nil {
return prepareError(err, errors.New("unable to update tenant"))
return restapi.ErrorWithContext(ctx, err, errors.New("unable to update tenant"))
}
return nil
}
func getTenantSecurityResponse(session *models.Principal, params operator_api.TenantSecurityParams) (*models.TenantSecurityResponse, *models.Error) {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
opClient := &operatorClient{
client: opClientClientSet,
}
minTenant, err := getTenant(ctx, opClient, params.Namespace, params.Tenant)
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
// get Kubernetes Client
clientSet, err := cluster.K8sClient(session.STSSessionToken)
@@ -917,27 +928,27 @@ func getTenantSecurityResponse(session *models.Principal, params operator_api.Te
client: clientSet,
}
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
info, err := getTenantSecurity(ctx, &k8sClient, minTenant)
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
return info, nil
}
func getUpdateTenantSecurityResponse(session *models.Principal, params operator_api.UpdateTenantSecurityParams) *models.Error {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
if err != nil {
return prepareError(err)
return restapi.ErrorWithContext(ctx, err)
}
// get Kubernetes Client
clientSet, err := cluster.K8sClient(session.STSSessionToken)
if err != nil {
return prepareError(err)
return restapi.ErrorWithContext(ctx, err)
}
k8sClient := k8sClient{
client: clientSet,
@@ -946,7 +957,7 @@ func getUpdateTenantSecurityResponse(session *models.Principal, params operator_
client: opClientClientSet,
}
if err := updateTenantSecurity(ctx, opClient, &k8sClient, params.Namespace, params); err != nil {
return prepareError(err, errors.New("unable to update tenant"))
return restapi.ErrorWithContext(ctx, err, errors.New("unable to update tenant"))
}
return nil
}
@@ -1117,36 +1128,36 @@ func listTenants(ctx context.Context, operatorClient OperatorClientI, namespace
}
func getListAllTenantsResponse(session *models.Principal, params operator_api.ListAllTenantsParams) (*models.ListTenantsResponse, *models.Error) {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
opClient := &operatorClient{
client: opClientClientSet,
}
listT, err := listTenants(ctx, opClient, "", params.Limit)
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
return listT, nil
}
// getListTenantsResponse list tenants by namespace
func getListTenantsResponse(session *models.Principal, params operator_api.ListTenantsParams) (*models.ListTenantsResponse, *models.Error) {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
opClient := &operatorClient{
client: opClientClientSet,
}
listT, err := listTenants(ctx, opClient, params.Namespace, params.Limit)
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
return listT, nil
}
@@ -1233,27 +1244,27 @@ func removeAnnotations(annotationsOne, annotationsTwo map[string]string) map[str
}
func getUpdateTenantResponse(session *models.Principal, params operator_api.UpdateTenantParams) *models.Error {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
if err != nil {
return prepareError(err)
return restapi.ErrorWithContext(ctx, err)
}
// get Kubernetes Client
clientSet, err := cluster.K8sClient(session.STSSessionToken)
if err != nil {
return prepareError(err)
return restapi.ErrorWithContext(ctx, err)
}
opClient := &operatorClient{
client: opClientClientSet,
}
httpC := &utils2.HTTPClient{
httpC := &utils2.Client{
Client: &http.Client{
Timeout: 4 * time.Second,
},
}
if err := updateTenantAction(ctx, opClient, clientSet.CoreV1(), httpC, params.Namespace, params); err != nil {
return prepareError(err, errors.New("unable to update tenant"))
return restapi.ErrorWithContext(ctx, err, errors.New("unable to update tenant"))
}
return nil
}
@@ -1284,17 +1295,17 @@ func addTenantPool(ctx context.Context, operatorClient OperatorClientI, params o
}
func getTenantAddPoolResponse(session *models.Principal, params operator_api.TenantAddPoolParams) *models.Error {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
if err != nil {
return prepareError(err)
return restapi.ErrorWithContext(ctx, err)
}
opClient := &operatorClient{
client: opClientClientSet,
}
if err := addTenantPool(ctx, opClient, params); err != nil {
return prepareError(err, errors.New("unable to add pool"))
return restapi.ErrorWithContext(ctx, err, errors.New("unable to add pool"))
}
return nil
}
@@ -1302,16 +1313,16 @@ func getTenantAddPoolResponse(session *models.Principal, params operator_api.Ten
// getTenantUsageResponse returns the usage of a tenant
func getTenantUsageResponse(session *models.Principal, params operator_api.GetTenantUsageParams) (*models.TenantUsage, *models.Error) {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
if err != nil {
return nil, prepareError(err, errorUnableToGetTenantUsage)
return nil, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage)
}
clientSet, err := cluster.K8sClient(session.STSSessionToken)
if err != nil {
return nil, prepareError(err, errorUnableToGetTenantUsage)
return nil, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage)
}
opClient := &operatorClient{
@@ -1323,7 +1334,7 @@ func getTenantUsageResponse(session *models.Principal, params operator_api.GetTe
minTenant, err := getTenant(ctx, opClient, params.Namespace, params.Tenant)
if err != nil {
return nil, prepareError(err, errorUnableToGetTenantUsage)
return nil, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage)
}
minTenant.EnsureDefaults()
@@ -1336,7 +1347,7 @@ func getTenantUsageResponse(session *models.Principal, params operator_api.GetTe
svcURL,
)
if err != nil {
return nil, prepareError(err, errorUnableToGetTenantUsage)
return nil, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage)
}
// create a minioClient interface implementation
// defining the client to be used
@@ -1344,7 +1355,7 @@ func getTenantUsageResponse(session *models.Principal, params operator_api.GetTe
// serialize output
adminInfo, err := restapi.GetAdminInfo(ctx, adminClient)
if err != nil {
return nil, prepareError(err, errorUnableToGetTenantUsage)
return nil, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage)
}
info := &models.TenantUsage{Used: adminInfo.Usage, DiskUsed: adminInfo.DisksUsage}
return info, nil
@@ -1353,12 +1364,12 @@ func getTenantUsageResponse(session *models.Principal, params operator_api.GetTe
// getTenantLogsResponse returns the logs of a tenant
func getTenantLogsResponse(session *models.Principal, params operator_api.GetTenantLogsParams) (*models.TenantLogs, *models.Error) {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
if err != nil {
return nil, prepareError(err, errorUnableToGetTenantLogs)
return nil, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantLogs)
}
opClient := &operatorClient{
@@ -1367,7 +1378,7 @@ func getTenantLogsResponse(session *models.Principal, params operator_api.GetTen
minTenant, err := getTenant(ctx, opClient, params.Namespace, params.Tenant)
if err != nil {
return nil, prepareError(err, errorUnableToGetTenantLogs)
return nil, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantLogs)
}
if minTenant.Spec.Log == nil {
retval := &models.TenantLogs{
@@ -1450,12 +1461,12 @@ func getTenantLogsResponse(session *models.Principal, params operator_api.GetTen
// setTenantLogsResponse returns the logs of a tenant
func setTenantLogsResponse(session *models.Principal, params operator_api.SetTenantLogsParams) (bool, *models.Error) {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
if err != nil {
return false, prepareError(err, errorUnableToGetTenantUsage)
return false, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage)
}
opClient := &operatorClient{
@@ -1464,7 +1475,7 @@ func setTenantLogsResponse(session *models.Principal, params operator_api.SetTen
minTenant, err := getTenant(ctx, opClient, params.Namespace, params.Tenant)
if err != nil {
return false, prepareError(err, errorUnableToGetTenantUsage)
return false, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage)
}
var labels = make(map[string]string)
@@ -1493,7 +1504,7 @@ func setTenantLogsResponse(session *models.Principal, params operator_api.SetTen
if reflect.TypeOf(params.Data.LogCPURequest).Kind() == reflect.String && params.Data.LogCPURequest != "0Gi" && params.Data.LogCPURequest != "" {
cpuQuantity, err := resource.ParseQuantity(params.Data.LogCPURequest)
if err != nil {
return false, prepareError(err)
return false, restapi.ErrorWithContext(ctx, err)
}
logResourceRequest["cpu"] = cpuQuantity
minTenant.Spec.Log.Resources.Requests = logResourceRequest
@@ -1501,7 +1512,7 @@ func setTenantLogsResponse(session *models.Principal, params operator_api.SetTen
if reflect.TypeOf(params.Data.LogMemRequest).Kind() == reflect.String {
memQuantity, err := resource.ParseQuantity(params.Data.LogMemRequest)
if err != nil {
return false, prepareError(err)
return false, restapi.ErrorWithContext(ctx, err)
}
logResourceRequest["memory"] = memQuantity
@@ -1538,7 +1549,7 @@ func setTenantLogsResponse(session *models.Principal, params operator_api.SetTen
if reflect.TypeOf(params.Data.LogDBCPURequest).Kind() == reflect.String && params.Data.LogDBCPURequest != "0Gi" && params.Data.LogDBCPURequest != "" {
dbCPUQuantity, err := resource.ParseQuantity(params.Data.LogDBCPURequest)
if err != nil {
return false, prepareError(err)
return false, restapi.ErrorWithContext(ctx, err)
}
logDBResourceRequest["cpu"] = dbCPUQuantity
minTenant.Spec.Log.Db.Resources.Requests = logDBResourceRequest
@@ -1546,7 +1557,7 @@ func setTenantLogsResponse(session *models.Principal, params operator_api.SetTen
if reflect.TypeOf(params.Data.LogDBMemRequest).Kind() == reflect.String {
dbMemQuantity, err := resource.ParseQuantity(params.Data.LogDBMemRequest)
if err != nil {
return false, prepareError(err)
return false, restapi.ErrorWithContext(ctx, err)
}
logDBResourceRequest["memory"] = dbMemQuantity
minTenant.Spec.Log.Db.Resources.Requests = logDBResourceRequest
@@ -1609,7 +1620,7 @@ func setTenantLogsResponse(session *models.Principal, params operator_api.SetTen
_, err = opClient.TenantUpdate(ctx, minTenant, metav1.UpdateOptions{})
if err != nil {
return false, prepareError(err)
return false, restapi.ErrorWithContext(ctx, err)
}
return true, nil
}
@@ -1617,12 +1628,12 @@ func setTenantLogsResponse(session *models.Principal, params operator_api.SetTen
// enableTenantLoggingResponse enables Tenant Logging
func enableTenantLoggingResponse(session *models.Principal, params operator_api.EnableTenantLoggingParams) (bool, *models.Error) {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
if err != nil {
return false, prepareError(err, errorUnableToGetTenantUsage)
return false, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage)
}
opClient := &operatorClient{
@@ -1631,7 +1642,7 @@ func enableTenantLoggingResponse(session *models.Principal, params operator_api.
minTenant, err := getTenant(ctx, opClient, params.Namespace, params.Tenant)
if err != nil {
return false, prepareError(err, errorUnableToGetTenantUsage)
return false, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage)
}
minTenant.EnsureDefaults()
@@ -1670,7 +1681,7 @@ func enableTenantLoggingResponse(session *models.Principal, params operator_api.
_, err = opClient.TenantUpdate(ctx, minTenant, metav1.UpdateOptions{})
if err != nil {
return false, prepareError(err)
return false, restapi.ErrorWithContext(ctx, err)
}
return true, nil
}
@@ -1678,15 +1689,15 @@ func enableTenantLoggingResponse(session *models.Principal, params operator_api.
// disableTenantLoggingResponse disables Tenant Logging
func disableTenantLoggingResponse(session *models.Principal, params operator_api.DisableTenantLoggingParams) (bool, *models.Error) {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
if err != nil {
return false, prepareError(err, errorUnableToGetTenantUsage)
return false, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage)
}
if err != nil {
return false, prepareError(err, errorUnableToGetTenantUsage)
return false, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage)
}
opClient := &operatorClient{
@@ -1695,31 +1706,31 @@ func disableTenantLoggingResponse(session *models.Principal, params operator_api
minTenant, err := getTenant(ctx, opClient, params.Namespace, params.Tenant)
if err != nil {
return false, prepareError(err, errorUnableToGetTenantUsage)
return false, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage)
}
minTenant.EnsureDefaults()
minTenant.Spec.Log = nil
_, err = opClient.TenantUpdate(ctx, minTenant, metav1.UpdateOptions{})
if err != nil {
return false, prepareError(err)
return false, restapi.ErrorWithContext(ctx, err)
}
return true, nil
}
func getTenantPodsResponse(session *models.Principal, params operator_api.GetTenantPodsParams) ([]*models.TenantPod, *models.Error) {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
clientset, err := cluster.K8sClient(session.STSSessionToken)
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
listOpts := metav1.ListOptions{
LabelSelector: fmt.Sprintf("%s=%s", miniov2.TenantLabel, params.Tenant),
}
pods, err := clientset.CoreV1().Pods(params.Namespace).List(ctx, listOpts)
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
retval := []*models.TenantPod{}
for _, pod := range pods.Items {
@@ -1743,35 +1754,35 @@ func getTenantPodsResponse(session *models.Principal, params operator_api.GetTen
}
func getPodLogsResponse(session *models.Principal, params operator_api.GetPodLogsParams) (string, *models.Error) {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
clientset, err := cluster.K8sClient(session.STSSessionToken)
if err != nil {
return "", prepareError(err)
return "", restapi.ErrorWithContext(ctx, err)
}
listOpts := &corev1.PodLogOptions{}
logs := clientset.CoreV1().Pods(params.Namespace).GetLogs(params.PodName, listOpts)
buff, err := logs.DoRaw(ctx)
if err != nil {
return "", prepareError(err)
return "", restapi.ErrorWithContext(ctx, err)
}
return string(buff), nil
}
func getPodEventsResponse(session *models.Principal, params operator_api.GetPodEventsParams) (models.EventListWrapper, *models.Error) {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
clientset, err := cluster.K8sClient(session.STSSessionToken)
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
pod, err := clientset.CoreV1().Pods(params.Namespace).Get(ctx, params.PodName, metav1.GetOptions{})
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
events, err := clientset.CoreV1().Events(params.Namespace).List(ctx, metav1.ListOptions{FieldSelector: fmt.Sprintf("involvedObject.uid=%s", pod.UID)})
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
retval := models.EventListWrapper{}
for i := 0; i < len(events.Items); i++ {
@@ -1791,12 +1802,12 @@ func getPodEventsResponse(session *models.Principal, params operator_api.GetPodE
//get values for prometheus metrics
func getTenantMonitoringResponse(session *models.Principal, params operator_api.GetTenantMonitoringParams) (*models.TenantMonitoringInfo, *models.Error) {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
opClient := &operatorClient{
@@ -1805,7 +1816,7 @@ func getTenantMonitoringResponse(session *models.Principal, params operator_api.
minInst, err := opClient.TenantGet(ctx, params.Namespace, params.Tenant, metav1.GetOptions{})
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
monitoringInfo := &models.TenantMonitoringInfo{}
@@ -1886,12 +1897,12 @@ func getTenantMonitoringResponse(session *models.Principal, params operator_api.
//sets tenant Prometheus monitoring cofiguration fields to values provided
func setTenantMonitoringResponse(session *models.Principal, params operator_api.SetTenantMonitoringParams) (bool, *models.Error) {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
if err != nil {
return false, prepareError(err, errorUnableToGetTenantUsage)
return false, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage)
}
opClient := &operatorClient{
@@ -1900,7 +1911,7 @@ func setTenantMonitoringResponse(session *models.Principal, params operator_api.
minTenant, err := getTenant(ctx, opClient, params.Namespace, params.Tenant)
if err != nil {
return false, prepareError(err, errorUnableToGetTenantUsage)
return false, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage)
}
if params.Data.Toggle {
@@ -1916,7 +1927,7 @@ func setTenantMonitoringResponse(session *models.Principal, params operator_api.
}
_, err = opClient.TenantUpdate(ctx, minTenant, metav1.UpdateOptions{})
if err != nil {
return false, prepareError(err)
return false, restapi.ErrorWithContext(ctx, err)
}
return true, nil
}
@@ -1944,7 +1955,7 @@ func setTenantMonitoringResponse(session *models.Principal, params operator_api.
if params.Data.MonitoringCPURequest != "" {
cpuQuantity, err := resource.ParseQuantity(params.Data.MonitoringCPURequest)
if err != nil {
return false, prepareError(err)
return false, restapi.ErrorWithContext(ctx, err)
}
monitoringResourceRequest["cpu"] = cpuQuantity
}
@@ -1952,7 +1963,7 @@ func setTenantMonitoringResponse(session *models.Principal, params operator_api.
if params.Data.MonitoringMemRequest != "" {
memQuantity, err := resource.ParseQuantity(params.Data.MonitoringMemRequest)
if err != nil {
return false, prepareError(err)
return false, restapi.ErrorWithContext(ctx, err)
}
monitoringResourceRequest["memory"] = memQuantity
}
@@ -1977,7 +1988,7 @@ func setTenantMonitoringResponse(session *models.Principal, params operator_api.
minTenant.Spec.Prometheus.ServiceAccountName = params.Data.ServiceAccountName
_, err = opClient.TenantUpdate(ctx, minTenant, metav1.UpdateOptions{})
if err != nil {
return false, prepareError(err)
return false, restapi.ErrorWithContext(ctx, err)
}
return true, nil
@@ -2422,11 +2433,11 @@ func parseNodeSelectorTerm(term *corev1.NodeSelectorTerm) *models.NodeSelectorTe
}
func getTenantUpdatePoolResponse(session *models.Principal, params operator_api.TenantUpdatePoolsParams) (*models.Tenant, *models.Error) {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
opClient := &operatorClient{
@@ -2436,7 +2447,7 @@ func getTenantUpdatePoolResponse(session *models.Principal, params operator_api.
t, err := updateTenantPools(ctx, opClient, params.Namespace, params.Tenant, params.Body.Pools)
if err != nil {
restapi.LogError("error updating Tenant's pools: %v", err)
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
// parse it to models.Tenant
@@ -2487,20 +2498,19 @@ func updateTenantPools(
}
func getTenantYAML(session *models.Principal, params operator_api.GetTenantYAMLParams) (*models.TenantYAML, *models.Error) {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
// get Kubernetes Client
opClient, err := cluster.OperatorClient(session.STSSessionToken)
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
tenant, err := opClient.MinioV2().Tenants(params.Namespace).Get(params.HTTPRequest.Context(), params.Tenant, metav1.GetOptions{})
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
// remove managed fields
tenant.ManagedFields = []metav1.ManagedFieldsEntry{}
//yb, err := yaml.Marshal(tenant)
j8sJSONSerializer := k8sJson.NewSerializerWithOptions(
k8sJson.DefaultMetaFactory, nil, nil,
@@ -2514,7 +2524,7 @@ func getTenantYAML(session *models.Principal, params operator_api.GetTenantYAMLP
err = j8sJSONSerializer.Encode(tenant, buf)
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
yb := buf.String()
@@ -2523,6 +2533,8 @@ func getTenantYAML(session *models.Principal, params operator_api.GetTenantYAMLP
}
func getUpdateTenantYAML(session *models.Principal, params operator_api.PutTenantYAMLParams) *models.Error {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
// https://godoc.org/k8s.io/apimachinery/pkg/runtime#Scheme
scheme := runtime.NewScheme()
@@ -2540,12 +2552,12 @@ func getUpdateTenantYAML(session *models.Principal, params operator_api.PutTenan
// get Kubernetes Client
opClient, err := cluster.OperatorClient(session.STSSessionToken)
if err != nil {
return prepareError(err)
return restapi.ErrorWithContext(ctx, err)
}
tenant, err := opClient.MinioV2().Tenants(params.Namespace).Get(params.HTTPRequest.Context(), params.Tenant, metav1.GetOptions{})
if err != nil {
return prepareError(err)
return restapi.ErrorWithContext(ctx, err)
}
upTenant := tenant.DeepCopy()
// only update safe fields: spec, metadata.finalizers, metadata.labels and metadata.annotations
@@ -2563,23 +2575,23 @@ func getUpdateTenantYAML(session *models.Principal, params operator_api.PutTenan
}
func getTenantEventsResponse(session *models.Principal, params operator_api.GetTenantEventsParams) (models.EventListWrapper, *models.Error) {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
client, err := cluster.OperatorClient(session.STSSessionToken)
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
clientset, err := cluster.K8sClient(session.STSSessionToken)
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
tenant, err := client.MinioV2().Tenants(params.Namespace).Get(ctx, params.Tenant, metav1.GetOptions{})
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
events, err := clientset.CoreV1().Events(params.Namespace).List(ctx, metav1.ListOptions{FieldSelector: fmt.Sprintf("involvedObject.uid=%s", tenant.UID)})
if err != nil {
return nil, prepareError(err)
return nil, restapi.ErrorWithContext(ctx, err)
}
retval := models.EventListWrapper{}
for _, event := range events.Items {
@@ -2603,7 +2615,7 @@ func getUpdateDomainsResponse(session *models.Principal, params operator_api.Upd
operatorCli, err := cluster.OperatorClient(session.STSSessionToken)
if err != nil {
return prepareError(err)
return restapi.ErrorWithContext(ctx, err)
}
opClient := &operatorClient{
@@ -2613,7 +2625,7 @@ func getUpdateDomainsResponse(session *models.Principal, params operator_api.Upd
err = updateTenantDomains(ctx, opClient, params.Namespace, params.Tenant, params.Body.Domains)
if err != nil {
return prepareError(err)
return restapi.ErrorWithContext(ctx, err)
}
return nil

View File

@@ -27,6 +27,8 @@ import (
"strconv"
"time"
xerrors "github.com/minio/console/restapi"
"github.com/minio/console/operatorapi/operations/operator_api"
"errors"
@@ -108,25 +110,25 @@ func tenantUpdateCertificates(ctx context.Context, operatorClient OperatorClient
// getTenantUpdateCertificatesResponse wrapper of tenantUpdateCertificates
func getTenantUpdateCertificatesResponse(session *models.Principal, params operator_api.TenantUpdateCertificateParams) *models.Error {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
// get Kubernetes Client
clientSet, err := cluster.K8sClient(session.STSSessionToken)
if err != nil {
return prepareError(err, errorUnableToUpdateTenantCertificates)
return xerrors.ErrorWithContext(ctx, err, xerrors.ErrUnableToUpdateTenantCertificates)
}
k8sClient := k8sClient{
client: clientSet,
}
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
if err != nil {
return prepareError(err, errorUnableToUpdateTenantCertificates)
return xerrors.ErrorWithContext(ctx, err, xerrors.ErrUnableToUpdateTenantCertificates)
}
opClient := operatorClient{
client: opClientClientSet,
}
if err := tenantUpdateCertificates(ctx, &opClient, &k8sClient, params.Namespace, params); err != nil {
return prepareError(err, errorUnableToUpdateTenantCertificates)
return xerrors.ErrorWithContext(ctx, err, xerrors.ErrUnableToUpdateTenantCertificates)
}
return nil
}
@@ -239,42 +241,42 @@ func tenantUpdateEncryption(ctx context.Context, operatorClient OperatorClientI,
// getTenantDeleteEncryptionResponse is a wrapper for tenantDeleteEncryption
func getTenantDeleteEncryptionResponse(session *models.Principal, params operator_api.TenantDeleteEncryptionParams) *models.Error {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
if err != nil {
return prepareError(err, errorDeletingEncryptionConfig)
return xerrors.ErrorWithContext(ctx, err, xerrors.ErrDeletingEncryptionConfig)
}
opClient := operatorClient{
client: opClientClientSet,
}
if err := tenantDeleteEncryption(ctx, &opClient, params.Namespace, params); err != nil {
return prepareError(err, errorDeletingEncryptionConfig)
return xerrors.ErrorWithContext(ctx, err, xerrors.ErrDeletingEncryptionConfig)
}
return nil
}
// getTenantUpdateEncryptionResponse is a wrapper for tenantUpdateEncryption
func getTenantUpdateEncryptionResponse(session *models.Principal, params operator_api.TenantUpdateEncryptionParams) *models.Error {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
// get Kubernetes Client
clientSet, err := cluster.K8sClient(session.STSSessionToken)
if err != nil {
return prepareError(err, errorUpdatingEncryptionConfig)
return xerrors.ErrorWithContext(ctx, err, xerrors.ErrUpdatingEncryptionConfig)
}
k8sClient := k8sClient{
client: clientSet,
}
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
if err != nil {
return prepareError(err, errorUpdatingEncryptionConfig)
return xerrors.ErrorWithContext(ctx, err, xerrors.ErrUpdatingEncryptionConfig)
}
opClient := operatorClient{
client: opClientClientSet,
}
if err := tenantUpdateEncryption(ctx, &opClient, &k8sClient, params.Namespace, params); err != nil {
return prepareError(err, errorUpdatingEncryptionConfig)
return xerrors.ErrorWithContext(ctx, err, xerrors.ErrUpdatingEncryptionConfig)
}
return nil
}
@@ -453,26 +455,26 @@ func tenantEncryptionInfo(ctx context.Context, operatorClient OperatorClientI, c
// getTenantEncryptionResponse is a wrapper for tenantEncryptionInfo
func getTenantEncryptionInfoResponse(session *models.Principal, params operator_api.TenantEncryptionInfoParams) (*models.EncryptionConfigurationResponse, *models.Error) {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
// get Kubernetes Client
clientSet, err := cluster.K8sClient(session.STSSessionToken)
if err != nil {
return nil, prepareError(err, errorEncryptionConfigNotFound)
return nil, xerrors.ErrorWithContext(ctx, err, xerrors.ErrEncryptionConfigNotFound)
}
k8sClient := k8sClient{
client: clientSet,
}
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
if err != nil {
return nil, prepareError(err, errorEncryptionConfigNotFound)
return nil, xerrors.ErrorWithContext(ctx, err, xerrors.ErrEncryptionConfigNotFound)
}
opClient := operatorClient{
client: opClientClientSet,
}
configuration, err := tenantEncryptionInfo(ctx, &opClient, &k8sClient, params.Namespace, params)
if err != nil {
return nil, prepareError(err, errorEncryptionConfigNotFound)
return nil, xerrors.ErrorWithContext(ctx, err, xerrors.ErrEncryptionConfigNotFound)
}
return configuration, nil
}
@@ -541,8 +543,8 @@ func createOrReplaceSecrets(ctx context.Context, clientSet K8sClientI, ns string
// delete secret with same name if exists
err := clientSet.deleteSecret(ctx, ns, secret.Name, metav1.DeleteOptions{})
if err != nil {
// log the error if any and continue
LogError("deleting secret name %s failed: %v, continuing..", secret.Name, err)
// log the errors if any and continue
xerrors.LogError("deleting secret name %s failed: %v, continuing..", secret.Name, err)
}
k8sSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
@@ -579,8 +581,8 @@ func createOrReplaceExternalCertSecrets(ctx context.Context, clientSet K8sClient
// delete secret with same name if exists
err := clientSet.deleteSecret(ctx, ns, keyPairSecretName, metav1.DeleteOptions{})
if err != nil {
// log the error if any and continue
LogError("deleting secret name %s failed: %v, continuing..", keyPairSecretName, err)
// log the errors if any and continue
xerrors.LogError("deleting secret name %s failed: %v, continuing..", keyPairSecretName, err)
}
imm := true
tlsCrt, err := base64.StdEncoding.DecodeString(*keyPair.Crt)
@@ -625,13 +627,13 @@ func createOrReplaceExternalCertSecrets(ctx context.Context, clientSet K8sClient
func createOrReplaceKesConfigurationSecrets(ctx context.Context, clientSet K8sClientI, ns string, encryptionCfg *models.EncryptionConfiguration, kesConfigurationSecretName, kesClientCertSecretName, tenantName string) (*corev1.LocalObjectReference, *miniov2.LocalCertificateReference, error) {
// delete KES configuration secret if exists
if err := clientSet.deleteSecret(ctx, ns, kesConfigurationSecretName, metav1.DeleteOptions{}); err != nil {
// log the error if any and continue
LogError("deleting secret name %s failed: %v, continuing..", kesConfigurationSecretName, err)
// log the errors if any and continue
xerrors.LogError("deleting secret name %s failed: %v, continuing..", kesConfigurationSecretName, err)
}
// delete KES client cert secret if exists
if err := clientSet.deleteSecret(ctx, ns, kesClientCertSecretName, metav1.DeleteOptions{}); err != nil {
// log the error if any and continue
LogError("deleting secret name %s failed: %v, continuing..", kesClientCertSecretName, err)
// log the errors if any and continue
xerrors.LogError("deleting secret name %s failed: %v, continuing..", kesClientCertSecretName, err)
}
// if autoCert is enabled then Operator will generate the client certificates, calculate the client cert identity
// and pass it to KES via the ${MINIO_KES_IDENTITY} variable

View File

@@ -28,7 +28,7 @@ import (
"testing"
"time"
"github.com/minio/console/pkg/utils"
xhttp "github.com/minio/console/pkg/http"
"github.com/minio/console/operatorapi/operations/operator_api"
@@ -897,7 +897,7 @@ func Test_UpdateTenantAction(t *testing.T) {
type args struct {
ctx context.Context
operatorClient OperatorClientI
httpCl utils.HTTPClientI
httpCl xhttp.ClientI
nameSpace string
tenantName string
mockTenantPatch func(ctx context.Context, namespace string, tenantName string, pt types.PatchType, data []byte, options metav1.PatchOptions) (*miniov2.Tenant, error)

View File

@@ -17,20 +17,24 @@
package operatorapi
import (
"context"
"net/http"
"time"
errors "github.com/minio/console/restapi"
xhttp "github.com/minio/console/pkg/http"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/models"
"github.com/minio/console/operatorapi/operations"
"github.com/minio/console/operatorapi/operations/user_api"
"net/http"
"time"
"github.com/minio/console/pkg/utils"
)
func registerVersionHandlers(api *operations.OperatorAPI) {
api.UserAPICheckMinIOVersionHandler = user_api.CheckMinIOVersionHandlerFunc(func(params user_api.CheckMinIOVersionParams) middleware.Responder {
versionResponse, err := getVersionResponse()
versionResponse, err := getVersionResponse(params)
if err != nil {
return user_api.NewCheckMinIOVersionDefault(int(err.Code)).WithPayload(err)
}
@@ -39,13 +43,15 @@ func registerVersionHandlers(api *operations.OperatorAPI) {
}
// getSessionResponse parse the token of the current session and returns a list of allowed actions to render in the UI
func getVersionResponse() (*models.CheckOperatorVersionResponse, *models.Error) {
ver, err := utils.GetLatestMinIOImage(&utils.HTTPClient{
func getVersionResponse(params user_api.CheckMinIOVersionParams) (*models.CheckOperatorVersionResponse, *models.Error) {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
ver, err := utils.GetLatestMinIOImage(&xhttp.Client{
Client: &http.Client{
Timeout: 15 * time.Second,
}})
if err != nil {
return nil, prepareError(err)
return nil, errors.ErrorWithContext(ctx, err)
}
return &models.CheckOperatorVersionResponse{
LatestVersion: *ver,

View File

@@ -21,6 +21,8 @@ import (
"fmt"
"sort"
errors "github.com/minio/console/restapi"
miniov1 "github.com/minio/operator/pkg/apis/minio.min.io/v1"
"github.com/go-openapi/runtime/middleware"
@@ -33,7 +35,7 @@ import (
func registerVolumesHandlers(api *operations.OperatorAPI) {
api.OperatorAPIListPVCsHandler = operator_api.ListPVCsHandlerFunc(func(params operator_api.ListPVCsParams, session *models.Principal) middleware.Responder {
payload, err := getPVCsResponse(session)
payload, err := getPVCsResponse(session, params)
if err != nil {
return operator_api.NewListPVCsDefault(int(err.Code)).WithPayload(err)
@@ -72,13 +74,13 @@ func registerVolumesHandlers(api *operations.OperatorAPI) {
}
func getPVCsResponse(session *models.Principal) (*models.ListPVCsResponse, *models.Error) {
ctx, cancel := context.WithCancel(context.Background())
func getPVCsResponse(session *models.Principal, params operator_api.ListPVCsParams) (*models.ListPVCsResponse, *models.Error) {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
clientset, err := cluster.K8sClient(session.STSSessionToken)
if err != nil {
return nil, prepareError(err)
return nil, errors.ErrorWithContext(ctx, err)
}
// Filter Tenant PVCs. They keep their v1 tenant annotation
@@ -90,7 +92,7 @@ func getPVCsResponse(session *models.Principal) (*models.ListPVCsResponse, *mode
listAllPvcs, err2 := clientset.CoreV1().PersistentVolumeClaims("").List(ctx, listOpts)
if err2 != nil {
return nil, prepareError(err2)
return nil, errors.ErrorWithContext(ctx, err2)
}
var ListPVCs []*models.PvcsListResponse
@@ -121,12 +123,12 @@ func getPVCsResponse(session *models.Principal) (*models.ListPVCsResponse, *mode
}
func getPVCsForTenantResponse(session *models.Principal, params operator_api.ListPVCsForTenantParams) (*models.ListPVCsResponse, *models.Error) {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
clientset, err := cluster.K8sClient(session.STSSessionToken)
if err != nil {
return nil, prepareError(err)
return nil, errors.ErrorWithContext(ctx, err)
}
// Filter Tenant PVCs. They keep their v1 tenant annotation
@@ -138,7 +140,7 @@ func getPVCsForTenantResponse(session *models.Principal, params operator_api.Lis
listAllPvcs, err2 := clientset.CoreV1().PersistentVolumeClaims(params.Namespace).List(ctx, listOpts)
if err2 != nil {
return nil, prepareError(err2)
return nil, errors.ErrorWithContext(ctx, err2)
}
var ListPVCs []*models.PvcsListResponse
@@ -169,37 +171,37 @@ func getPVCsForTenantResponse(session *models.Principal, params operator_api.Lis
}
func getDeletePVCResponse(session *models.Principal, params operator_api.DeletePVCParams) *models.Error {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
// get Kubernetes Client
clientset, err := cluster.K8sClient(session.STSSessionToken)
if err != nil {
return prepareError(err)
return errors.ErrorWithContext(ctx, err)
}
listOpts := metav1.ListOptions{
LabelSelector: fmt.Sprintf("v1.min.io/tenant=%s", params.Tenant),
FieldSelector: fmt.Sprintf("metadata.name=%s", params.PVCName),
}
if err = clientset.CoreV1().PersistentVolumeClaims(params.Namespace).DeleteCollection(ctx, metav1.DeleteOptions{}, listOpts); err != nil {
return prepareError(err)
return errors.ErrorWithContext(ctx, err)
}
return nil
}
func getPVCEventsResponse(session *models.Principal, params operator_api.GetPVCEventsParams) (models.EventListWrapper, *models.Error) {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
clientset, err := cluster.K8sClient(session.STSSessionToken)
if err != nil {
return nil, prepareError(err)
return nil, errors.ErrorWithContext(ctx, err)
}
PVC, err := clientset.CoreV1().PersistentVolumeClaims(params.Namespace).Get(ctx, params.PVCName, metav1.GetOptions{})
if err != nil {
return nil, prepareError(err)
return nil, errors.ErrorWithContext(ctx, err)
}
events, err := clientset.CoreV1().Events(params.Namespace).List(ctx, metav1.ListOptions{FieldSelector: fmt.Sprintf("involvedObject.uid=%s", PVC.UID)})
if err != nil {
return nil, prepareError(err)
return nil, errors.ErrorWithContext(ctx, err)
}
retval := models.EventListWrapper{}
for i := 0; i < len(events.Items); i++ {

View File

@@ -89,11 +89,14 @@ func SessionTokenAuthenticate(token string) (*TokenClaims, error) {
if token == "" {
return nil, ErrNoAuthToken
}
// decrypt encrypted token
claimTokens, err := decryptClaims(token)
decryptedToken, err := DecryptToken(token)
if err != nil {
// we print decryption token error information for debugging purposes
// we return a generic error that doesn't give any information to attackers
// fail decrypting token
return nil, errReadingToken
}
claimTokens, err := ParseClaimsFromToken(string(decryptedToken))
if err != nil {
// fail unmarshalling token into data structure
return nil, errReadingToken
}
// claimsTokens contains the decrypted JWT for Console
@@ -136,21 +139,26 @@ func encryptClaims(credentials *TokenClaims) (string, error) {
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
// decryptClaims() receives base64 encoded ciphertext, decode it, decrypt it (AES-GCM) and produces a *TokenClaims object
func decryptClaims(ciphertext string) (*TokenClaims, error) {
// ParseClaimsFromToken receive token claims in string format, then unmarshal them to produce a *TokenClaims object
func ParseClaimsFromToken(claims string) (*TokenClaims, error) {
tokenClaims := &TokenClaims{}
if err := json.Unmarshal([]byte(claims), tokenClaims); err != nil {
return nil, err
}
return tokenClaims, nil
}
// DecryptToken receives base64 encoded ciphertext, decode it, decrypt it (AES-GCM) and produces []byte
func DecryptToken(ciphertext string) (plaintext []byte, err error) {
decoded, err := base64.StdEncoding.DecodeString(ciphertext)
if err != nil {
return nil, err
}
plaintext, err := decrypt(decoded, []byte{})
plaintext, err = decrypt(decoded, []byte{})
if err != nil {
return nil, err
}
tokenClaims := &TokenClaims{}
if err = json.Unmarshal(plaintext, tokenClaims); err != nil {
return nil, err
}
return tokenClaims, nil
return plaintext, nil
}
const (

View File

@@ -332,3 +332,12 @@ func GetAllCertificatesAndCAs() (*x509.CertPool, []*x509.Certificate, *xcerts.Ma
}
return rootCAs, publicCerts, certsManager, nil
}
// EnsureCertAndKey checks if both client certificate and key paths are provided
func EnsureCertAndKey(clientCert, clientKey string) error {
if (clientCert != "" && clientKey == "") ||
(clientCert == "" && clientKey != "") {
return errors.New("cert and key must be specified as a pair")
}
return nil
}

23
pkg/http/headers.go Normal file
View File

@@ -0,0 +1,23 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package http
// Standard S3 HTTP response constants
const (
ETag = "ETag"
ContentType = "Content-Type"
)

View File

@@ -14,40 +14,61 @@
// 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
package http
import (
"io"
"io/ioutil"
"net/http"
)
// HTTPClientI interface with all functions to be implemented
// ClientI interface with all functions to be implemented
// by mock when testing, it should include all HttpClient respective api calls
// that are used within this project.
type HTTPClientI interface {
type ClientI interface {
Get(url string) (resp *http.Response, err error)
Post(url, contentType string, body io.Reader) (resp *http.Response, err error)
Do(req *http.Request) (*http.Response, error)
}
// HTTPClient Interface implementation
// Client is an HTTP Interface implementation
//
// Define the structure of a http client and define the functions that are actually used
type HTTPClient struct {
type Client struct {
Client *http.Client
}
// Get implements http.Client.Get()
func (c *HTTPClient) Get(url string) (resp *http.Response, err error) {
func (c *Client) Get(url string) (resp *http.Response, err error) {
return c.Client.Get(url)
}
// Post implements http.Client.Post()
func (c *HTTPClient) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) {
func (c *Client) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) {
return c.Client.Post(url, contentType, body)
}
// Do implement http.Client.Do()
func (c *HTTPClient) Do(req *http.Request) (*http.Response, error) {
func (c *Client) Do(req *http.Request) (*http.Response, error) {
return c.Client.Do(req)
}
// DrainBody close non nil response with any response Body.
// convenient wrapper to drain any remaining data on response body.
//
// Subsequently this allows golang http RoundTripper
// to re-use the same connection for future requests.
func DrainBody(respBody io.ReadCloser) {
// Callers should close resp.Body when done reading from it.
// If resp.Body is not closed, the Client's underlying RoundTripper
// (typically Transport) may not be able to re-use a persistent TCP
// connection to the server for a subsequent "keep-alive" request.
if respBody != nil {
// Drain any remaining Body and then close the connection.
// Without this closing connection would disallow re-using
// the same connection for future uses.
// - http://stackoverflow.com/a/17961593/4465767
defer respBody.Close()
io.Copy(ioutil.Discard, respBody)
}
}

228
pkg/logger/audit.go Normal file
View File

@@ -0,0 +1,228 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package logger
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"strconv"
"sync/atomic"
"time"
"github.com/minio/console/pkg/utils"
"github.com/minio/console/pkg/logger/message/audit"
)
// ResponseWriter - is a wrapper to trap the http response status code.
type ResponseWriter struct {
http.ResponseWriter
StatusCode int
// Log body of 4xx or 5xx responses
LogErrBody bool
// Log body of all responses
LogAllBody bool
TimeToFirstByte time.Duration
StartTime time.Time
// number of bytes written
bytesWritten int
// Internal recording buffer
headers bytes.Buffer
body bytes.Buffer
// Indicate if headers are written in the log
headersLogged bool
}
// NewResponseWriter - returns a wrapped response writer to trap
// http status codes for auditing purposes.
func NewResponseWriter(w http.ResponseWriter) *ResponseWriter {
return &ResponseWriter{
ResponseWriter: w,
StatusCode: http.StatusOK,
StartTime: time.Now().UTC(),
}
}
func (lrw *ResponseWriter) Write(p []byte) (int, error) {
if !lrw.headersLogged {
// We assume the response code to be '200 OK' when WriteHeader() is not called,
// that way following Golang HTTP response behavior.
lrw.WriteHeader(http.StatusOK)
}
n, err := lrw.ResponseWriter.Write(p)
lrw.bytesWritten += n
if lrw.TimeToFirstByte == 0 {
lrw.TimeToFirstByte = time.Now().UTC().Sub(lrw.StartTime)
}
if (lrw.LogErrBody && lrw.StatusCode >= http.StatusBadRequest) || lrw.LogAllBody {
// Always logging error responses.
lrw.body.Write(p)
}
if err != nil {
return n, err
}
return n, err
}
// Write the headers into the given buffer
func (lrw *ResponseWriter) writeHeaders(w io.Writer, statusCode int, headers http.Header) {
n, _ := fmt.Fprintf(w, "%d %s\n", statusCode, http.StatusText(statusCode))
lrw.bytesWritten += n
for k, v := range headers {
n, _ := fmt.Fprintf(w, "%s: %s\n", k, v[0])
lrw.bytesWritten += n
}
}
// BodyPlaceHolder returns a dummy body placeholder
var BodyPlaceHolder = []byte("<BODY>")
// Body - Return response body.
func (lrw *ResponseWriter) Body() []byte {
// If there was an error response or body logging is enabled
// then we return the body contents
if (lrw.LogErrBody && lrw.StatusCode >= http.StatusBadRequest) || lrw.LogAllBody {
return lrw.body.Bytes()
}
// ... otherwise we return the <BODY> place holder
return BodyPlaceHolder
}
// WriteHeader - writes http status code
func (lrw *ResponseWriter) WriteHeader(code int) {
if !lrw.headersLogged {
lrw.StatusCode = code
lrw.writeHeaders(&lrw.headers, code, lrw.ResponseWriter.Header())
lrw.headersLogged = true
lrw.ResponseWriter.WriteHeader(code)
}
}
// Flush - Calls the underlying Flush.
func (lrw *ResponseWriter) Flush() {
lrw.ResponseWriter.(http.Flusher).Flush()
}
// Size - reutrns the number of bytes written
func (lrw *ResponseWriter) Size() int {
return lrw.bytesWritten
}
// SetAuditEntry sets Audit info in the context.
func SetAuditEntry(ctx context.Context, audit *audit.Entry) context.Context {
if ctx == nil {
LogIf(context.Background(), fmt.Errorf("context is nil"))
return nil
}
return context.WithValue(ctx, utils.ContextAuditKey, audit)
}
// GetAuditEntry returns Audit entry if set.
func GetAuditEntry(ctx context.Context) *audit.Entry {
if ctx != nil {
r, ok := ctx.Value(utils.ContextAuditKey).(*audit.Entry)
if ok {
return r
}
r = &audit.Entry{
Version: audit.Version,
//DeploymentID: globalDeploymentID,
Time: time.Now().UTC(),
}
SetAuditEntry(ctx, r)
return r
}
return nil
}
// AuditLog - logs audit logs to all audit targets.
func AuditLog(ctx context.Context, w *ResponseWriter, r *http.Request, reqClaims map[string]interface{}, filterKeys ...string) {
// Fast exit if there is not audit target configured
if atomic.LoadInt32(&nAuditTargets) == 0 {
return
}
var entry audit.Entry
if w != nil && r != nil {
reqInfo := GetReqInfo(ctx)
if reqInfo == nil {
return
}
entry = audit.ToEntry(w, r, reqClaims, GetGlobalDeploymentID())
// indicates all requests for this API call are inbound
entry.Trigger = "incoming"
for _, filterKey := range filterKeys {
delete(entry.ReqClaims, filterKey)
delete(entry.ReqQuery, filterKey)
delete(entry.ReqHeader, filterKey)
delete(entry.RespHeader, filterKey)
}
var (
statusCode int
timeToResponse time.Duration
timeToFirstByte time.Duration
outputBytes int64 = -1 // -1: unknown output bytes
)
if w != nil {
statusCode = w.StatusCode
timeToResponse = time.Now().UTC().Sub(w.StartTime)
timeToFirstByte = w.TimeToFirstByte
outputBytes = int64(w.Size())
}
entry.API.Path = r.URL.Path
entry.API.Status = http.StatusText(statusCode)
entry.API.StatusCode = statusCode
entry.API.Method = r.Method
entry.API.InputBytes = r.ContentLength
entry.API.OutputBytes = outputBytes
entry.RequestID = reqInfo.RequestID
entry.API.TimeToResponse = strconv.FormatInt(timeToResponse.Nanoseconds(), 10) + "ns"
entry.Tags = reqInfo.GetTagsMap()
// ttfb will be recorded only for GET requests, Ignore such cases where ttfb will be empty.
if timeToFirstByte != 0 {
entry.API.TimeToFirstByte = strconv.FormatInt(timeToFirstByte.Nanoseconds(), 10) + "ns"
}
} else {
auditEntry := GetAuditEntry(ctx)
if auditEntry != nil {
entry = *auditEntry
}
}
if anonFlag {
entry.SessionID = hashString(entry.SessionID)
entry.RemoteHost = hashString(entry.RemoteHost)
}
// Send audit logs only to http targets.
for _, t := range AuditTargets() {
if err := t.Send(entry, string(All)); err != nil {
LogAlwaysIf(context.Background(), fmt.Errorf("event(%v) was not sent to Audit target (%v): %v", entry, t, err), All)
}
}
}

60
pkg/logger/color/color.go Normal file
View File

@@ -0,0 +1,60 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package color
import (
"fmt"
"github.com/fatih/color"
)
// global colors.
var (
// Check if we stderr, stdout are dumb terminals, we do not apply
// ansi coloring on dumb terminals.
IsTerminal = func() bool {
return !color.NoColor
}
Bold = func() func(a ...interface{}) string {
if IsTerminal() {
return color.New(color.Bold).SprintFunc()
}
return fmt.Sprint
}()
FgRed = func() func(a ...interface{}) string {
if IsTerminal() {
return color.New(color.FgRed).SprintFunc()
}
return fmt.Sprint
}()
BgRed = func() func(format string, a ...interface{}) string {
if IsTerminal() {
return color.New(color.BgRed).SprintfFunc()
}
return fmt.Sprintf
}()
FgWhite = func() func(format string, a ...interface{}) string {
if IsTerminal() {
return color.New(color.FgWhite).SprintfFunc()
}
return fmt.Sprintf
}()
)

213
pkg/logger/config.go Normal file
View File

@@ -0,0 +1,213 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package logger
import (
"errors"
"strconv"
"strings"
"github.com/google/uuid"
"github.com/minio/console/pkg/logger/config"
"github.com/minio/console/pkg/logger/target/http"
"github.com/minio/pkg/env"
)
// NewConfig - initialize new logger config.
func NewConfig() Config {
cfg := Config{
HTTP: make(map[string]http.Config),
AuditWebhook: make(map[string]http.Config),
}
return cfg
}
func lookupLoggerWebhookConfig() (Config, error) {
cfg := NewConfig()
envs := env.List(EnvLoggerWebhookEndpoint)
var loggerTargets []string
for _, k := range envs {
target := strings.TrimPrefix(k, EnvLoggerWebhookEndpoint+config.Default)
if target == EnvLoggerWebhookEndpoint {
target = config.Default
}
loggerTargets = append(loggerTargets, target)
}
// Load HTTP logger from the environment if found
for _, target := range loggerTargets {
if v, ok := cfg.HTTP[target]; ok && v.Enabled {
// This target is already enabled using the
// legacy environment variables, ignore.
continue
}
enableEnv := EnvLoggerWebhookEnable
if target != config.Default {
enableEnv = EnvLoggerWebhookEnable + config.Default + target
}
enable, err := config.ParseBool(env.Get(enableEnv, ""))
if err != nil || !enable {
continue
}
endpointEnv := EnvLoggerWebhookEndpoint
if target != config.Default {
endpointEnv = EnvLoggerWebhookEndpoint + config.Default + target
}
authTokenEnv := EnvLoggerWebhookAuthToken
if target != config.Default {
authTokenEnv = EnvLoggerWebhookAuthToken + config.Default + target
}
clientCertEnv := EnvLoggerWebhookClientCert
if target != config.Default {
clientCertEnv = EnvLoggerWebhookClientCert + config.Default + target
}
clientKeyEnv := EnvLoggerWebhookClientKey
if target != config.Default {
clientKeyEnv = EnvLoggerWebhookClientKey + config.Default + target
}
err = config.EnsureCertAndKey(env.Get(clientCertEnv, ""), env.Get(clientKeyEnv, ""))
if err != nil {
return cfg, err
}
queueSizeEnv := EnvLoggerWebhookQueueSize
if target != config.Default {
queueSizeEnv = EnvLoggerWebhookQueueSize + config.Default + target
}
queueSize, err := strconv.Atoi(env.Get(queueSizeEnv, "100000"))
if err != nil {
return cfg, err
}
if queueSize <= 0 {
return cfg, errors.New("invalid queue_size value")
}
cfg.HTTP[target] = http.Config{
Enabled: true,
Endpoint: env.Get(endpointEnv, ""),
AuthToken: env.Get(authTokenEnv, ""),
ClientCert: env.Get(clientCertEnv, ""),
ClientKey: env.Get(clientKeyEnv, ""),
QueueSize: queueSize,
}
}
return cfg, nil
}
func lookupAuditWebhookConfig() (Config, error) {
cfg := NewConfig()
var loggerAuditTargets []string
envs := env.List(EnvAuditWebhookEndpoint)
for _, k := range envs {
target := strings.TrimPrefix(k, EnvAuditWebhookEndpoint+config.Default)
if target == EnvAuditWebhookEndpoint {
target = config.Default
}
loggerAuditTargets = append(loggerAuditTargets, target)
}
for _, target := range loggerAuditTargets {
if v, ok := cfg.AuditWebhook[target]; ok && v.Enabled {
// This target is already enabled using the
// legacy environment variables, ignore.
continue
}
enableEnv := EnvAuditWebhookEnable
if target != config.Default {
enableEnv = EnvAuditWebhookEnable + config.Default + target
}
enable, err := config.ParseBool(env.Get(enableEnv, ""))
if err != nil || !enable {
continue
}
endpointEnv := EnvAuditWebhookEndpoint
if target != config.Default {
endpointEnv = EnvAuditWebhookEndpoint + config.Default + target
}
authTokenEnv := EnvAuditWebhookAuthToken
if target != config.Default {
authTokenEnv = EnvAuditWebhookAuthToken + config.Default + target
}
clientCertEnv := EnvAuditWebhookClientCert
if target != config.Default {
clientCertEnv = EnvAuditWebhookClientCert + config.Default + target
}
clientKeyEnv := EnvAuditWebhookClientKey
if target != config.Default {
clientKeyEnv = EnvAuditWebhookClientKey + config.Default + target
}
err = config.EnsureCertAndKey(env.Get(clientCertEnv, ""), env.Get(clientKeyEnv, ""))
if err != nil {
return cfg, err
}
queueSizeEnv := EnvAuditWebhookQueueSize
if target != config.Default {
queueSizeEnv = EnvAuditWebhookQueueSize + config.Default + target
}
queueSize, err := strconv.Atoi(env.Get(queueSizeEnv, "100000"))
if err != nil {
return cfg, err
}
if queueSize <= 0 {
return cfg, errors.New("invalid queue_size value")
}
cfg.AuditWebhook[target] = http.Config{
Enabled: true,
Endpoint: env.Get(endpointEnv, ""),
AuthToken: env.Get(authTokenEnv, ""),
ClientCert: env.Get(clientCertEnv, ""),
ClientKey: env.Get(clientKeyEnv, ""),
QueueSize: queueSize,
}
}
return cfg, nil
}
// LookupConfigForSubSys - lookup logger config, override with ENVs if set, for the given sub-system
func LookupConfigForSubSys(subSys string) (cfg Config, err error) {
switch subSys {
case config.LoggerWebhookSubSys:
if cfg, err = lookupLoggerWebhookConfig(); err != nil {
return cfg, err
}
case config.AuditWebhookSubSys:
if cfg, err = lookupAuditWebhookConfig(); err != nil {
return cfg, err
}
}
return cfg, nil
}
// GetGlobalDeploymentID :
func GetGlobalDeploymentID() string {
if globalDeploymentID != "" {
return globalDeploymentID
}
globalDeploymentID = env.Get(EnvGlobalDeploymentID, mustGetUUID())
return globalDeploymentID
}
// mustGetUUID - get a random UUID.
func mustGetUUID() string {
u, err := uuid.NewRandom()
if err != nil {
CriticalIf(GlobalContext, err)
}
return u.String()
}

View File

@@ -0,0 +1,80 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package config
import (
"encoding/json"
"fmt"
"strconv"
"strings"
)
// BoolFlag - wrapper bool type.
type BoolFlag bool
// String - returns string of BoolFlag.
func (bf BoolFlag) String() string {
if bf {
return "on"
}
return "off"
}
// MarshalJSON - converts BoolFlag into JSON data.
func (bf BoolFlag) MarshalJSON() ([]byte, error) {
return json.Marshal(bf.String())
}
// UnmarshalJSON - parses given data into BoolFlag.
func (bf *BoolFlag) UnmarshalJSON(data []byte) (err error) {
var s string
if err = json.Unmarshal(data, &s); err == nil {
b := BoolFlag(true)
if s == "" {
// Empty string is treated as valid.
*bf = b
} else if b, err = ParseBoolFlag(s); err == nil {
*bf = b
}
}
return err
}
// ParseBool returns the boolean value represented by the string.
func ParseBool(str string) (bool, error) {
switch str {
case "1", "t", "T", "true", "TRUE", "True", "on", "ON", "On":
return true, nil
case "0", "f", "F", "false", "FALSE", "False", "off", "OFF", "Off":
return false, nil
}
if strings.EqualFold(str, "enabled") {
return true, nil
}
if strings.EqualFold(str, "disabled") {
return false, nil
}
return false, fmt.Errorf("ParseBool: parsing '%s': %w", str, strconv.ErrSyntax)
}
// ParseBoolFlag - parses string into BoolFlag.
func ParseBoolFlag(s string) (bf BoolFlag, err error) {
b, err := ParseBool(s)
return BoolFlag(b), err
}

View File

@@ -0,0 +1,126 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package config
import "testing"
// Test BoolFlag.String()
func TestBoolFlagString(t *testing.T) {
var bf BoolFlag
testCases := []struct {
flag BoolFlag
expectedResult string
}{
{bf, "off"},
{BoolFlag(true), "on"},
{BoolFlag(false), "off"},
}
for _, testCase := range testCases {
str := testCase.flag.String()
if testCase.expectedResult != str {
t.Fatalf("expected: %v, got: %v", testCase.expectedResult, str)
}
}
}
// Test BoolFlag.MarshalJSON()
func TestBoolFlagMarshalJSON(t *testing.T) {
var bf BoolFlag
testCases := []struct {
flag BoolFlag
expectedResult string
}{
{bf, `"off"`},
{BoolFlag(true), `"on"`},
{BoolFlag(false), `"off"`},
}
for _, testCase := range testCases {
data, _ := testCase.flag.MarshalJSON()
if testCase.expectedResult != string(data) {
t.Fatalf("expected: %v, got: %v", testCase.expectedResult, string(data))
}
}
}
// Test BoolFlag.UnmarshalJSON()
func TestBoolFlagUnmarshalJSON(t *testing.T) {
testCases := []struct {
data []byte
expectedResult BoolFlag
expectedErr bool
}{
{[]byte(`{}`), BoolFlag(false), true},
{[]byte(`["on"]`), BoolFlag(false), true},
{[]byte(`"junk"`), BoolFlag(false), true},
{[]byte(`""`), BoolFlag(true), false},
{[]byte(`"on"`), BoolFlag(true), false},
{[]byte(`"off"`), BoolFlag(false), false},
{[]byte(`"true"`), BoolFlag(true), false},
{[]byte(`"false"`), BoolFlag(false), false},
{[]byte(`"ON"`), BoolFlag(true), false},
{[]byte(`"OFF"`), BoolFlag(false), false},
}
for _, testCase := range testCases {
var flag BoolFlag
err := (&flag).UnmarshalJSON(testCase.data)
if !testCase.expectedErr && err != nil {
t.Fatalf("error: expected = <nil>, got = %v", err)
}
if testCase.expectedErr && err == nil {
t.Fatalf("error: expected error, got = <nil>")
}
if err == nil && testCase.expectedResult != flag {
t.Fatalf("result: expected: %v, got: %v", testCase.expectedResult, flag)
}
}
}
// Test ParseBoolFlag()
func TestParseBoolFlag(t *testing.T) {
testCases := []struct {
flagStr string
expectedResult BoolFlag
expectedErr bool
}{
{"", BoolFlag(false), true},
{"junk", BoolFlag(false), true},
{"true", BoolFlag(true), false},
{"false", BoolFlag(false), false},
{"ON", BoolFlag(true), false},
{"OFF", BoolFlag(false), false},
{"on", BoolFlag(true), false},
{"off", BoolFlag(false), false},
}
for _, testCase := range testCases {
bf, err := ParseBoolFlag(testCase.flagStr)
if !testCase.expectedErr && err != nil {
t.Fatalf("error: expected = <nil>, got = %v", err)
}
if testCase.expectedErr && err == nil {
t.Fatalf("error: expected error, got = <nil>")
}
if err == nil && testCase.expectedResult != bf {
t.Fatalf("result: expected: %v, got: %v", testCase.expectedResult, bf)
}
}
}

View File

@@ -0,0 +1,30 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package config
import (
"errors"
)
// EnsureCertAndKey checks if both client certificate and key paths are provided
func EnsureCertAndKey(clientCert, clientKey string) error {
if (clientCert != "" && clientKey == "") ||
(clientCert == "" && clientKey != "") {
return errors.New("cert and key must be specified as a pair")
}
return nil
}

View File

@@ -0,0 +1,34 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package config
import (
"github.com/minio/madmin-go"
)
// Default keys
const (
Default = madmin.Default
Enable = madmin.EnableKey
License = "license" // Deprecated Dec 2021
)
// Top level config constants.
const (
LoggerWebhookSubSys = "logger_webhook"
AuditWebhookSubSys = "audit_webhook"
)

223
pkg/logger/console.go Normal file
View File

@@ -0,0 +1,223 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package logger
import (
"encoding/json"
"fmt"
"os"
"strings"
"time"
"github.com/minio/console/pkg/logger/color"
"github.com/minio/console/pkg/logger/message/log"
c "github.com/minio/pkg/console"
)
// ConsoleLoggerTgt is a stringified value to represent console logging
const ConsoleLoggerTgt = "console+http"
// Logger interface describes the methods that need to be implemented to satisfy the interface requirements.
type Logger interface {
json(msg string, args ...interface{})
quiet(msg string, args ...interface{})
pretty(msg string, args ...interface{})
}
func consoleLog(console Logger, msg string, args ...interface{}) {
switch {
case jsonFlag:
// Strip escape control characters from json message
msg = ansiRE.ReplaceAllLiteralString(msg, "")
console.json(msg, args...)
case quietFlag:
console.quiet(msg+"\n", args...)
default:
console.pretty(msg+"\n", args...)
}
}
// Fatal prints only fatal errors message with no stack trace
// it will be called for input validation failures
func Fatal(err error, msg string, data ...interface{}) {
fatal(err, msg, data...)
}
func fatal(err error, msg string, data ...interface{}) {
var errMsg string
if msg != "" {
errMsg = errorFmtFunc(fmt.Sprintf(msg, data...), err, jsonFlag)
} else {
errMsg = err.Error()
}
consoleLog(fatalMessage, errMsg)
}
var fatalMessage fatalMsg
type fatalMsg struct{}
func (f fatalMsg) json(msg string, args ...interface{}) {
var message string
if msg != "" {
message = fmt.Sprintf(msg, args...)
} else {
message = fmt.Sprint(args...)
}
logJSON, err := json.Marshal(&log.Entry{
Level: FatalLvl.String(),
Message: message,
Time: time.Now().UTC(),
Trace: &log.Trace{Message: message, Source: []string{getSource(6)}},
})
if err != nil {
panic(err)
}
fmt.Println(string(logJSON))
os.Exit(1)
}
func (f fatalMsg) quiet(msg string, args ...interface{}) {
f.pretty(msg, args...)
}
var (
logTag = "ERROR"
logBanner = color.BgRed(color.FgWhite(color.Bold(logTag))) + " "
emptyBanner = color.BgRed(strings.Repeat(" ", len(logTag))) + " "
bannerWidth = len(logTag) + 1
)
func (f fatalMsg) pretty(msg string, args ...interface{}) {
// Build the passed errors message
errMsg := fmt.Sprintf(msg, args...)
tagPrinted := false
// Print the errors message: the following code takes care
// of splitting errors text and always pretty printing the
// red banner along with the errors message. Since the errors
// message itself contains some colored text, we needed
// to use some ANSI control escapes to cursor color state
// and freely move in the screen.
for _, line := range strings.Split(errMsg, "\n") {
if len(line) == 0 {
// No more text to print, just quit.
break
}
for {
// Save the attributes of the current cursor helps
// us save the text color of the passed errors message
ansiSaveAttributes()
// Print banner with or without the log tag
if !tagPrinted {
c.Print(logBanner)
tagPrinted = true
} else {
c.Print(emptyBanner)
}
// Restore the text color of the errors message
ansiRestoreAttributes()
ansiMoveRight(bannerWidth)
// Continue errors message printing
c.Println(line)
break
}
}
// Exit because this is a fatal errors message
os.Exit(1)
}
type infoMsg struct{}
var info infoMsg
func (i infoMsg) json(msg string, args ...interface{}) {
var message string
if msg != "" {
message = fmt.Sprintf(msg, args...)
} else {
message = fmt.Sprint(args...)
}
logJSON, err := json.Marshal(&log.Entry{
Level: InformationLvl.String(),
Message: message,
Time: time.Now().UTC(),
})
if err != nil {
panic(err)
}
fmt.Println(string(logJSON))
}
func (i infoMsg) quiet(msg string, args ...interface{}) {
}
func (i infoMsg) pretty(msg string, args ...interface{}) {
if msg == "" {
c.Println(args...)
}
c.Printf(msg, args...)
}
type errorMsg struct{}
var errorm errorMsg
func (i errorMsg) json(msg string, args ...interface{}) {
var message string
if msg != "" {
message = fmt.Sprintf(msg, args...)
} else {
message = fmt.Sprint(args...)
}
logJSON, err := json.Marshal(&log.Entry{
Level: ErrorLvl.String(),
Message: message,
Time: time.Now().UTC(),
Trace: &log.Trace{Message: message, Source: []string{getSource(6)}},
})
if err != nil {
panic(err)
}
fmt.Println(string(logJSON))
}
func (i errorMsg) quiet(msg string, args ...interface{}) {
i.pretty(msg, args...)
}
func (i errorMsg) pretty(msg string, args ...interface{}) {
if msg == "" {
c.Println(args...)
}
c.Printf(msg, args...)
c.Printf("\n")
}
// Error :
func Error(msg string, data ...interface{}) {
consoleLog(errorm, msg, data...)
}
// Info :
func Info(msg string, data ...interface{}) {
consoleLog(info, msg, data...)
}

56
pkg/logger/const.go Normal file
View File

@@ -0,0 +1,56 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package logger
import (
"context"
"github.com/minio/console/pkg/logger/target/http"
)
// Audit/Logger constants
const (
EnvLoggerJSONEnable = "CONSOLE_LOGGER_JSON_ENABLE"
EnvLoggerAnonymousEnable = "CONSOLE_LOGGER_ANONYMOUS_ENABLE"
EnvLoggerQuietEnable = "CONSOLE_LOGGER_QUIET_ENABLE"
EnvGlobalDeploymentID = "CONSOLE_GLOBAL_DEPLOYMENT_ID"
EnvLoggerWebhookEnable = "CONSOLE_LOGGER_WEBHOOK_ENABLE"
EnvLoggerWebhookEndpoint = "CONSOLE_LOGGER_WEBHOOK_ENDPOINT"
EnvLoggerWebhookAuthToken = "CONSOLE_LOGGER_WEBHOOK_AUTH_TOKEN"
EnvLoggerWebhookClientCert = "CONSOLE_LOGGER_WEBHOOK_CLIENT_CERT"
EnvLoggerWebhookClientKey = "CONSOLE_LOGGER_WEBHOOK_CLIENT_KEY"
EnvLoggerWebhookQueueSize = "CONSOLE_LOGGER_WEBHOOK_QUEUE_SIZE"
EnvAuditWebhookEnable = "CONSOLE_AUDIT_WEBHOOK_ENABLE"
EnvAuditWebhookEndpoint = "CONSOLE_AUDIT_WEBHOOK_ENDPOINT"
EnvAuditWebhookAuthToken = "CONSOLE_AUDIT_WEBHOOK_AUTH_TOKEN"
EnvAuditWebhookClientCert = "CONSOLE_AUDIT_WEBHOOK_CLIENT_CERT"
EnvAuditWebhookClientKey = "CONSOLE_AUDIT_WEBHOOK_CLIENT_KEY"
EnvAuditWebhookQueueSize = "CONSOLE_AUDIT_WEBHOOK_QUEUE_SIZE"
)
// Config console and http logger targets
type Config struct {
HTTP map[string]http.Config `json:"http"`
AuditWebhook map[string]http.Config `json:"audit"`
}
var (
globalDeploymentID string
GlobalContext context.Context
)

480
pkg/logger/logger.go Normal file
View File

@@ -0,0 +1,480 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package logger
import (
"context"
"crypto/tls"
"encoding/hex"
"errors"
"fmt"
"go/build"
"net/http"
"path/filepath"
"reflect"
"runtime"
"strings"
"syscall"
"time"
"github.com/minio/pkg/env"
"github.com/minio/console/pkg"
"github.com/minio/pkg/certs"
"github.com/minio/console/pkg/logger/config"
"github.com/minio/console/pkg/logger/message/log"
"github.com/minio/highwayhash"
"github.com/minio/minio-go/v7/pkg/set"
)
// HighwayHash key for logging in anonymous mode
var magicHighwayHash256Key = []byte("\x4b\xe7\x34\xfa\x8e\x23\x8a\xcd\x26\x3e\x83\xe6\xbb\x96\x85\x52\x04\x0f\x93\x5d\xa3\x9f\x44\x14\x97\xe0\x9d\x13\x22\xde\x36\xa0")
// Disable disables all logging, false by default. (used for "go test")
var Disable = false
// Level type
type Level int8
// Enumerated level types
const (
InformationLvl Level = iota + 1
ErrorLvl
FatalLvl
)
var trimStrings []string
// TimeFormat - logging time format.
const TimeFormat string = "15:04:05 MST 01/02/2006"
var matchingFuncNames = [...]string{
"http.HandlerFunc.ServeHTTP",
"cmd.serverMain",
"cmd.StartGateway",
// add more here ..
}
func (level Level) String() string {
var lvlStr string
switch level {
case InformationLvl:
lvlStr = "INFO"
case ErrorLvl:
lvlStr = "ERROR"
case FatalLvl:
lvlStr = "FATAL"
}
return lvlStr
}
// quietFlag: Hide startup messages if enabled
// jsonFlag: Display in JSON format, if enabled
var (
quietFlag, jsonFlag, anonFlag bool
// Custom function to format errors
errorFmtFunc func(string, error, bool) string
)
// EnableQuiet - turns quiet option on.
func EnableQuiet() {
quietFlag = true
}
// EnableJSON - outputs logs in json format.
func EnableJSON() {
jsonFlag = true
quietFlag = true
}
// EnableAnonymous - turns anonymous flag
// to avoid printing sensitive information.
func EnableAnonymous() {
anonFlag = true
}
// IsAnonymous - returns true if anonFlag is true
func IsAnonymous() bool {
return anonFlag
}
// IsJSON - returns true if jsonFlag is true
func IsJSON() bool {
return jsonFlag
}
// IsQuiet - returns true if quietFlag is true
func IsQuiet() bool {
return quietFlag
}
// RegisterError registers the specified rendering function. This latter
// will be called for a pretty rendering of fatal errors.
func RegisterError(f func(string, error, bool) string) {
errorFmtFunc = f
}
// Remove any duplicates and return unique entries.
func uniqueEntries(paths []string) []string {
m := make(set.StringSet)
for _, p := range paths {
if !m.Contains(p) {
m.Add(p)
}
}
return m.ToSlice()
}
// Init sets the trimStrings to possible GOPATHs
// and GOROOT directories. Also append github.com/minio/minio
// This is done to clean up the filename, when stack trace is
// displayed when an errors happens.
func Init(goPath string, goRoot string) {
var goPathList []string
var goRootList []string
var defaultgoPathList []string
var defaultgoRootList []string
pathSeperator := ":"
// Add all possible GOPATH paths into trimStrings
// Split GOPATH depending on the OS type
if runtime.GOOS == "windows" {
pathSeperator = ";"
}
goPathList = strings.Split(goPath, pathSeperator)
goRootList = strings.Split(goRoot, pathSeperator)
defaultgoPathList = strings.Split(build.Default.GOPATH, pathSeperator)
defaultgoRootList = strings.Split(build.Default.GOROOT, pathSeperator)
// Add trim string "{GOROOT}/src/" into trimStrings
trimStrings = []string{filepath.Join(runtime.GOROOT(), "src") + string(filepath.Separator)}
// Add all possible path from GOPATH=path1:path2...:pathN
// as "{path#}/src/" into trimStrings
for _, goPathString := range goPathList {
trimStrings = append(trimStrings, filepath.Join(goPathString, "src")+string(filepath.Separator))
}
for _, goRootString := range goRootList {
trimStrings = append(trimStrings, filepath.Join(goRootString, "src")+string(filepath.Separator))
}
for _, defaultgoPathString := range defaultgoPathList {
trimStrings = append(trimStrings, filepath.Join(defaultgoPathString, "src")+string(filepath.Separator))
}
for _, defaultgoRootString := range defaultgoRootList {
trimStrings = append(trimStrings, filepath.Join(defaultgoRootString, "src")+string(filepath.Separator))
}
// Remove duplicate entries.
trimStrings = uniqueEntries(trimStrings)
// Add "github.com/minio/minio" as the last to cover
// paths like "{GOROOT}/src/github.com/minio/minio"
// and "{GOPATH}/src/github.com/minio/minio"
trimStrings = append(trimStrings, filepath.Join("github.com", "minio", "minio")+string(filepath.Separator))
}
func trimTrace(f string) string {
for _, trimString := range trimStrings {
f = strings.TrimPrefix(filepath.ToSlash(f), filepath.ToSlash(trimString))
}
return filepath.FromSlash(f)
}
func getSource(level int) string {
pc, file, lineNumber, ok := runtime.Caller(level)
if ok {
// Clean up the common prefixes
file = trimTrace(file)
_, funcName := filepath.Split(runtime.FuncForPC(pc).Name())
return fmt.Sprintf("%v:%v:%v()", file, lineNumber, funcName)
}
return ""
}
// getTrace method - creates and returns stack trace
func getTrace(traceLevel int) []string {
var trace []string
pc, file, lineNumber, ok := runtime.Caller(traceLevel)
for ok && file != "" {
// Clean up the common prefixes
file = trimTrace(file)
// Get the function name
_, funcName := filepath.Split(runtime.FuncForPC(pc).Name())
// Skip duplicate traces that start with file name, "<autogenerated>"
// and also skip traces with function name that starts with "runtime."
if !strings.HasPrefix(file, "<autogenerated>") &&
!strings.HasPrefix(funcName, "runtime.") {
// Form and append a line of stack trace into a
// collection, 'trace', to build full stack trace
trace = append(trace, fmt.Sprintf("%v:%v:%v()", file, lineNumber, funcName))
// Ignore trace logs beyond the following conditions
for _, name := range matchingFuncNames {
if funcName == name {
return trace
}
}
}
traceLevel++
// Read stack trace information from PC
pc, file, lineNumber, ok = runtime.Caller(traceLevel)
}
return trace
}
// Return the highway hash of the passed string
func hashString(input string) string {
hh, _ := highwayhash.New(magicHighwayHash256Key)
hh.Write([]byte(input))
return hex.EncodeToString(hh.Sum(nil))
}
// Kind specifies the kind of errors log
type Kind string
const (
// Minio errors
Minio Kind = "CONSOLE"
// Application errors
Application Kind = "APPLICATION"
// All errors
All Kind = "ALL"
)
// LogAlwaysIf prints a detailed errors message during
// the execution of the server.
func LogAlwaysIf(ctx context.Context, err error, errKind ...interface{}) {
if err == nil {
return
}
logIf(ctx, err, errKind...)
}
// LogIf prints a detailed errors message during
// the execution of the server
func LogIf(ctx context.Context, err error, errKind ...interface{}) {
if err == nil {
return
}
if errors.Is(err, context.Canceled) {
return
}
logIf(ctx, err, errKind...)
}
// logIf prints a detailed errors message during
// the execution of the server.
func logIf(ctx context.Context, err error, errKind ...interface{}) {
if Disable {
return
}
logKind := string(Minio)
if len(errKind) > 0 {
if ek, ok := errKind[0].(Kind); ok {
logKind = string(ek)
}
}
req := GetReqInfo(ctx)
if req == nil {
req = &ReqInfo{API: "SYSTEM"}
}
kv := req.GetTags()
tags := make(map[string]interface{}, len(kv))
for _, entry := range kv {
tags[entry.Key] = entry.Val
}
// Get full stack trace
trace := getTrace(3)
// Get the cause for the Error
message := fmt.Sprintf("%v (%T)", err, err)
if req.DeploymentID == "" {
req.DeploymentID = GetGlobalDeploymentID()
}
entry := log.Entry{
DeploymentID: req.DeploymentID,
Level: ErrorLvl.String(),
LogKind: logKind,
RemoteHost: req.RemoteHost,
Host: req.Host,
RequestID: req.RequestID,
SessionID: req.SessionID,
UserAgent: req.UserAgent,
Time: time.Now().UTC(),
Trace: &log.Trace{
Message: message,
Source: trace,
Variables: tags,
},
}
if anonFlag {
entry.SessionID = hashString(entry.SessionID)
entry.RemoteHost = hashString(entry.RemoteHost)
entry.Trace.Message = reflect.TypeOf(err).String()
entry.Trace.Variables = make(map[string]interface{})
}
// Iterate over all logger targets to send the log entry
for _, t := range SystemTargets() {
if err := t.Send(entry, entry.LogKind); err != nil {
if consoleTgt != nil {
entry.Trace.Message = fmt.Sprintf("event(%#v) was not sent to Logger target (%#v): %#v", entry, t, err)
consoleTgt.Send(entry, entry.LogKind)
}
}
}
}
// ErrCritical is the value panic'd whenever CriticalIf is called.
var ErrCritical struct{}
// CriticalIf logs the provided errors on the console. It fails the
// current go-routine by causing a `panic(ErrCritical)`.
func CriticalIf(ctx context.Context, err error, errKind ...interface{}) {
if err != nil {
LogIf(ctx, err, errKind...)
panic(ErrCritical)
}
}
// FatalIf is similar to Fatal() but it ignores passed nil errors
func FatalIf(err error, msg string, data ...interface{}) {
if err == nil {
return
}
fatal(err, msg, data...)
}
func applyDynamicConfigForSubSys(ctx context.Context, transport *http.Transport, subSys string) error {
switch subSys {
case config.LoggerWebhookSubSys:
loggerCfg, err := LookupConfigForSubSys(config.LoggerWebhookSubSys)
if err != nil {
LogIf(ctx, fmt.Errorf("unable to load logger webhook config: %w", err))
return err
}
userAgent := getUserAgent()
for n, l := range loggerCfg.HTTP {
if l.Enabled {
l.LogOnce = LogOnceIf
l.UserAgent = userAgent
l.Transport = NewHTTPTransportWithClientCerts(transport, l.ClientCert, l.ClientKey)
loggerCfg.HTTP[n] = l
}
}
err = UpdateSystemTargets(loggerCfg)
if err != nil {
LogIf(ctx, fmt.Errorf("unable to update logger webhook config: %w", err))
return err
}
case config.AuditWebhookSubSys:
loggerCfg, err := LookupConfigForSubSys(config.AuditWebhookSubSys)
if err != nil {
LogIf(ctx, fmt.Errorf("unable to load audit webhook config: %w", err))
return err
}
userAgent := getUserAgent()
for n, l := range loggerCfg.AuditWebhook {
if l.Enabled {
l.LogOnce = LogOnceIf
l.UserAgent = userAgent
l.Transport = NewHTTPTransportWithClientCerts(transport, l.ClientCert, l.ClientKey)
loggerCfg.AuditWebhook[n] = l
}
}
err = UpdateAuditWebhookTargets(loggerCfg)
if err != nil {
LogIf(ctx, fmt.Errorf("Unable to update audit webhook targets: %w", err))
return err
}
}
return nil
}
// InitializeLogger :
func InitializeLogger(ctx context.Context, transport *http.Transport) error {
err := applyDynamicConfigForSubSys(ctx, transport, config.LoggerWebhookSubSys)
if err != nil {
return err
}
err = applyDynamicConfigForSubSys(ctx, transport, config.AuditWebhookSubSys)
if err != nil {
return err
}
if enable, _ := config.ParseBool(env.Get(EnvLoggerJSONEnable, "")); enable {
EnableJSON()
}
if enable, _ := config.ParseBool(env.Get(EnvLoggerAnonymousEnable, "")); enable {
EnableAnonymous()
}
if enable, _ := config.ParseBool(env.Get(EnvLoggerQuietEnable, "")); enable {
EnableQuiet()
}
return nil
}
func getUserAgent() string {
userAgentParts := []string{}
// Helper function to concisely append a pair of strings to a
// the user-agent slice.
uaAppend := func(p, q string) {
userAgentParts = append(userAgentParts, p, q)
}
uaAppend("Console (", runtime.GOOS)
uaAppend("; ", runtime.GOARCH)
uaAppend(") Console/", pkg.Version)
uaAppend(" Console/", pkg.ReleaseTag)
uaAppend(" Console/", pkg.CommitID)
return strings.Join(userAgentParts, "")
}
// NewHTTPTransportWithClientCerts returns a new http configuration
// used while communicating with the cloud backends.
func NewHTTPTransportWithClientCerts(parentTransport *http.Transport, clientCert, clientKey string) *http.Transport {
transport := parentTransport.Clone()
if clientCert != "" && clientKey != "" {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
c, err := certs.NewManager(ctx, clientCert, clientKey, tls.LoadX509KeyPair)
if err != nil {
LogIf(ctx, fmt.Errorf("failed to load client key and cert, please check your endpoint configuration: %s",
err.Error()))
}
if c != nil {
c.UpdateReloadDuration(10 * time.Second)
c.ReloadOnSignal(syscall.SIGHUP) // allow reloads upon SIGHUP
transport.TLSClientConfig.GetClientCertificate = c.GetClientCertificate
}
}
return transport
}

243
pkg/logger/logger_test.go Normal file
View File

@@ -0,0 +1,243 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package logger
import (
"context"
"fmt"
"net/http"
"os"
"testing"
)
func testServer(w http.ResponseWriter, r *http.Request) {
}
func TestInitializeLogger(t *testing.T) {
testServerWillStart := make(chan interface{})
http.HandleFunc("/", testServer)
go func() {
close(testServerWillStart)
err := http.ListenAndServe("127.0.0.1:1337", nil)
if err != nil {
return
}
}()
<-testServerWillStart
loggerWebhookEnable := fmt.Sprintf("%s_TEST", EnvLoggerWebhookEnable)
loggerWebhookEndpoint := fmt.Sprintf("%s_TEST", EnvLoggerWebhookEndpoint)
loggerWebhookAuthToken := fmt.Sprintf("%s_TEST", EnvLoggerWebhookAuthToken)
loggerWebhookClientCert := fmt.Sprintf("%s_TEST", EnvLoggerWebhookClientCert)
loggerWebhookClientKey := fmt.Sprintf("%s_TEST", EnvLoggerWebhookClientKey)
loggerWebhookQueueSize := fmt.Sprintf("%s_TEST", EnvLoggerWebhookQueueSize)
auditWebhookEnable := fmt.Sprintf("%s_TEST", EnvAuditWebhookEnable)
auditWebhookEndpoint := fmt.Sprintf("%s_TEST", EnvAuditWebhookEndpoint)
auditWebhookAuthToken := fmt.Sprintf("%s_TEST", EnvAuditWebhookAuthToken)
auditWebhookClientCert := fmt.Sprintf("%s_TEST", EnvAuditWebhookClientCert)
auditWebhookClientKey := fmt.Sprintf("%s_TEST", EnvAuditWebhookClientKey)
auditWebhookQueueSize := fmt.Sprintf("%s_TEST", EnvAuditWebhookQueueSize)
type args struct {
ctx context.Context
transport *http.Transport
}
tests := []struct {
name string
args args
wantErr bool
setEnvVars func()
unsetEnvVars func()
}{
{
name: "logger or auditlog is not enabled",
args: args{
ctx: context.Background(),
transport: http.DefaultTransport.(*http.Transport).Clone(),
},
wantErr: false,
setEnvVars: func() {
},
unsetEnvVars: func() {
},
},
{
name: "logger webhook initialized correctly",
args: args{
ctx: context.Background(),
transport: http.DefaultTransport.(*http.Transport).Clone(),
},
wantErr: false,
setEnvVars: func() {
os.Setenv(loggerWebhookEnable, "on")
os.Setenv(loggerWebhookEndpoint, "http://127.0.0.1:1337/logger")
os.Setenv(loggerWebhookAuthToken, "test")
os.Setenv(loggerWebhookClientCert, "")
os.Setenv(loggerWebhookClientKey, "")
os.Setenv(loggerWebhookQueueSize, "1000")
},
unsetEnvVars: func() {
os.Unsetenv(loggerWebhookEnable)
os.Unsetenv(loggerWebhookEndpoint)
os.Unsetenv(loggerWebhookAuthToken)
os.Unsetenv(loggerWebhookClientCert)
os.Unsetenv(loggerWebhookClientKey)
os.Unsetenv(loggerWebhookQueueSize)
},
},
{
name: "logger webhook failed to initialize",
args: args{
ctx: context.Background(),
transport: http.DefaultTransport.(*http.Transport).Clone(),
},
wantErr: true,
setEnvVars: func() {
os.Setenv(loggerWebhookEnable, "on")
os.Setenv(loggerWebhookEndpoint, "https://aklsjdakljdjkalsd.com")
os.Setenv(loggerWebhookAuthToken, "test")
os.Setenv(loggerWebhookClientCert, "")
os.Setenv(loggerWebhookClientKey, "")
os.Setenv(loggerWebhookQueueSize, "1000")
},
unsetEnvVars: func() {
os.Unsetenv(loggerWebhookEnable)
os.Unsetenv(loggerWebhookEndpoint)
os.Unsetenv(loggerWebhookAuthToken)
os.Unsetenv(loggerWebhookClientCert)
os.Unsetenv(loggerWebhookClientKey)
os.Unsetenv(loggerWebhookQueueSize)
},
},
{
name: "auditlog webhook initialized correctly",
args: args{
ctx: context.Background(),
transport: http.DefaultTransport.(*http.Transport).Clone(),
},
wantErr: false,
setEnvVars: func() {
os.Setenv(auditWebhookEnable, "on")
os.Setenv(auditWebhookEndpoint, "http://127.0.0.1:1337/audit")
os.Setenv(auditWebhookAuthToken, "test")
os.Setenv(auditWebhookClientCert, "")
os.Setenv(auditWebhookClientKey, "")
os.Setenv(auditWebhookQueueSize, "1000")
},
unsetEnvVars: func() {
os.Unsetenv(auditWebhookEnable)
os.Unsetenv(auditWebhookEndpoint)
os.Unsetenv(auditWebhookAuthToken)
os.Unsetenv(auditWebhookClientCert)
os.Unsetenv(auditWebhookClientKey)
os.Unsetenv(auditWebhookQueueSize)
},
},
{
name: "auditlog webhook failed to initialize",
args: args{
ctx: context.Background(),
transport: http.DefaultTransport.(*http.Transport).Clone(),
},
wantErr: true,
setEnvVars: func() {
os.Setenv(auditWebhookEnable, "on")
os.Setenv(auditWebhookEndpoint, "https://aklsjdakljdjkalsd.com")
os.Setenv(auditWebhookAuthToken, "test")
os.Setenv(auditWebhookClientCert, "")
os.Setenv(auditWebhookClientKey, "")
os.Setenv(auditWebhookQueueSize, "1000")
},
unsetEnvVars: func() {
os.Unsetenv(auditWebhookEnable)
os.Unsetenv(auditWebhookEndpoint)
os.Unsetenv(auditWebhookAuthToken)
os.Unsetenv(auditWebhookClientCert)
os.Unsetenv(auditWebhookClientKey)
os.Unsetenv(auditWebhookQueueSize)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.setEnvVars != nil {
tt.setEnvVars()
}
if err := InitializeLogger(tt.args.ctx, tt.args.transport); (err != nil) != tt.wantErr {
t.Errorf("InitializeLogger() error = %v, wantErr %v", err, tt.wantErr)
}
if tt.unsetEnvVars != nil {
tt.unsetEnvVars()
}
})
}
}
func TestEnableJSON(t *testing.T) {
tests := []struct {
name string
}{
{
name: "enable json",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
EnableJSON()
if !IsJSON() {
t.Errorf("EnableJSON() = %v, want %v", IsJSON(), true)
}
})
}
}
func TestEnableQuiet(t *testing.T) {
tests := []struct {
name string
}{
{
name: "enable quiet",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
EnableQuiet()
if !IsQuiet() {
t.Errorf("EnableQuiet() = %v, want %v", IsQuiet(), true)
}
})
}
}
func TestEnableAnonymous(t *testing.T) {
tests := []struct {
name string
}{
{
name: "enable anonymous",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
EnableAnonymous()
if !IsAnonymous() {
t.Errorf("EnableAnonymous() = %v, want %v", IsAnonymous(), true)
}
})
}
}

92
pkg/logger/logonce.go Normal file
View File

@@ -0,0 +1,92 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package logger
import (
"context"
"errors"
"net/http"
"sync"
"time"
)
// Holds a map of recently logged errors.
type logOnceType struct {
IDMap map[interface{}]error
sync.Mutex
}
// One log message per errors.
func (l *logOnceType) logOnceIf(ctx context.Context, err error, id interface{}, errKind ...interface{}) {
if err == nil {
return
}
l.Lock()
shouldLog := false
prevErr := l.IDMap[id]
if prevErr == nil {
l.IDMap[id] = err
shouldLog = true
} else if prevErr.Error() != err.Error() {
l.IDMap[id] = err
shouldLog = true
}
l.Unlock()
if shouldLog {
LogIf(ctx, err, errKind...)
}
}
// Cleanup the map every 30 minutes so that the log message is printed again for the user to notice.
func (l *logOnceType) cleanupRoutine() {
for {
l.Lock()
l.IDMap = make(map[interface{}]error)
l.Unlock()
time.Sleep(30 * time.Minute)
}
}
// Returns logOnceType
func newLogOnceType() *logOnceType {
l := &logOnceType{IDMap: make(map[interface{}]error)}
go l.cleanupRoutine()
return l
}
var logOnce = newLogOnceType()
// LogOnceIf - Logs notification errors - once per errors.
// id is a unique identifier for related log messages, refer to cmd/notification.go
// on how it is used.
func LogOnceIf(ctx context.Context, err error, id interface{}, errKind ...interface{}) {
if err == nil {
return
}
if errors.Is(err, context.Canceled) {
return
}
if err.Error() == http.ErrServerClosed.Error() || err.Error() == "disk not found" {
return
}
logOnce.logOnceIf(ctx, err, id, errKind...)
}

View File

@@ -0,0 +1,130 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package audit
import (
"net/http"
"os"
"strings"
"time"
"github.com/golang-jwt/jwt/v4"
"github.com/minio/console/pkg/utils"
xhttp "github.com/minio/console/pkg/http"
)
// Version - represents the current version of audit log structure.
const Version = "1"
// ObjectVersion object version key/versionId
type ObjectVersion struct {
ObjectName string `json:"objectName"`
VersionID string `json:"versionId,omitempty"`
}
// Entry - audit entry logs.
type Entry struct {
Version string `json:"version"`
DeploymentID string `json:"deploymentid,omitempty"`
Time time.Time `json:"time"`
Trigger string `json:"trigger"`
API struct {
Path string `json:"path,omitempty"`
Status string `json:"status,omitempty"`
Method string `json:"method"`
StatusCode int `json:"statusCode,omitempty"`
InputBytes int64 `json:"rx"`
OutputBytes int64 `json:"tx"`
TimeToFirstByte string `json:"timeToFirstByte,omitempty"`
TimeToResponse string `json:"timeToResponse,omitempty"`
} `json:"api"`
RemoteHost string `json:"remotehost,omitempty"`
RequestID string `json:"requestID,omitempty"`
SessionID string `json:"sessionID,omitempty"`
UserAgent string `json:"userAgent,omitempty"`
ReqClaims map[string]interface{} `json:"requestClaims,omitempty"`
ReqQuery map[string]string `json:"requestQuery,omitempty"`
ReqHeader map[string]string `json:"requestHeader,omitempty"`
RespHeader map[string]string `json:"responseHeader,omitempty"`
Tags map[string]interface{} `json:"tags,omitempty"`
}
// NewEntry - constructs an audit entry object with some fields filled
func NewEntry(deploymentID string) Entry {
return Entry{
Version: Version,
DeploymentID: deploymentID,
Time: time.Now().UTC(),
}
}
// ToEntry - constructs an audit entry from a http request
func ToEntry(w http.ResponseWriter, r *http.Request, reqClaims map[string]interface{}, deploymentID string) Entry {
entry := NewEntry(deploymentID)
entry.RemoteHost = r.RemoteAddr
entry.UserAgent = r.UserAgent()
entry.ReqClaims = reqClaims
q := r.URL.Query()
reqQuery := make(map[string]string, len(q))
for k, v := range q {
reqQuery[k] = strings.Join(v, ",")
}
entry.ReqQuery = reqQuery
reqHeader := make(map[string]string, len(r.Header))
for k, v := range r.Header {
reqHeader[k] = strings.Join(v, ",")
}
entry.ReqHeader = reqHeader
wh := w.Header()
var requestID interface{}
requestID = r.Context().Value(utils.ContextRequestID)
if requestID == nil {
requestID, _ = utils.NewUUID()
}
entry.RequestID = requestID.(string)
if val := r.Context().Value(utils.ContextRequestUserID); val != nil {
sessionID := val.(string)
if os.Getenv("CONSOLE_OPERATOR_MODE") != "" && os.Getenv("CONSOLE_OPERATOR_MODE") == "on" {
claims := jwt.MapClaims{}
_, _ = jwt.ParseWithClaims(sessionID, claims, nil)
if sub, ok := claims["sub"]; ok {
sessionID = sub.(string)
}
}
entry.SessionID = sessionID
}
respHeader := make(map[string]string, len(wh))
for k, v := range wh {
respHeader[k] = strings.Join(v, ",")
}
entry.RespHeader = respHeader
if etag := respHeader[xhttp.ETag]; etag != "" {
respHeader[xhttp.ETag] = strings.Trim(etag, `"`)
}
return entry
}

View File

@@ -0,0 +1,126 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package audit
import (
"context"
"net/http"
"net/http/httptest"
"os"
"reflect"
"testing"
"time"
"github.com/minio/console/pkg/utils"
)
func TestNewEntry(t *testing.T) {
type args struct {
deploymentID string
}
tests := []struct {
name string
args args
want Entry
}{
{
name: "constructs an audit entry object with some fields filled",
args: args{
deploymentID: "1",
},
want: Entry{
Version: Version,
DeploymentID: "1",
Time: time.Now().UTC(),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := NewEntry(tt.args.deploymentID); got.DeploymentID != tt.want.DeploymentID {
t.Errorf("NewEntry() = %v, want %v", got, tt.want)
}
})
}
}
func TestToEntry(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/api/v1/tenants?test=xyz", nil)
req.Header.Set("Authorization", "xyz")
req.Header.Set("ETag", "\"ABCDE\"")
// applying context information
ctx := context.WithValue(req.Context(), utils.ContextRequestUserID, "eyJhbGciOiJSUzI1NiIsImtpZCI6Ing5cS0wSkEwQzFMWDJlRlR3dHo2b0t0NVNnRzJad0llMGVNczMxbjU0b2sifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJtaW5pby1vcGVyYXRvciIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJjb25zb2xlLXNhLXRva2VuLWJrZzZwIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImNvbnNvbGUtc2EiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiJhZTE2ZGVkNS01MmM3LTRkZTQtOWUxYS1iNmI4NGU2OGMzM2UiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6bWluaW8tb3BlcmF0b3I6Y29uc29sZS1zYSJ9.AjhzekAPC59SQVBQL5sr-1dqr57-jH8a5LVazpnEr_cC0JqT4jXYjdfbrZSF9yaL4gHRv2l0kOhBlrjRK7y-IpMbxE71Fne_lSzaptSuqgI5I9dFvpVfZWP1yMAqav8mrlUoWkWDq9IAkyH4bvvZrVgQJGgd5t9U_7DQCVwbkQvy0wGS5zoMcZhYenn_Ub1BoxWcviADQ1aY1wQju8OP0IOwKTIMXMQqciOFdJ9T5-tQEGUrikTu_tW-1shUHzOxBcEzGVtBvBy2OmbNnRFYogbhmp-Dze6EAi035bY32bfL7XKBUNCW6_3VbN_h3pQNAuT2NJOSKuhJ3cGldCB2zg")
req = req.WithContext(ctx)
w := httptest.NewRecorder()
w.Header().Set("Authorization", "xyz")
w.Header().Set("ETag", "\"ABCDE\"")
type args struct {
w http.ResponseWriter
r *http.Request
reqClaims map[string]interface{}
deploymentID string
}
tests := []struct {
name string
args args
want Entry
preFunc func()
postFunc func()
}{
{
preFunc: func() {
os.Setenv("CONSOLE_OPERATOR_MODE", "on")
},
postFunc: func() {
os.Unsetenv("CONSOLE_OPERATOR_MODE")
},
name: "constructs an audit entry from a http request",
args: args{
w: w,
r: req,
reqClaims: map[string]interface{}{},
deploymentID: "1",
},
want: Entry{
Version: "1",
DeploymentID: "1",
SessionID: "system:serviceaccount:minio-operator:console-sa",
ReqQuery: map[string]string{"test": "xyz"},
ReqHeader: map[string]string{"test": "xyz"},
RespHeader: map[string]string{"test": "xyz", "ETag": "ABCDE"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.preFunc != nil {
tt.preFunc()
}
if got := ToEntry(tt.args.w, tt.args.r, tt.args.reqClaims, tt.args.deploymentID); !reflect.DeepEqual(got, tt.want) {
t.Errorf("ToEntry() = %v, want %v", got, tt.want)
}
if tt.postFunc != nil {
tt.postFunc()
}
})
}
}

View File

@@ -0,0 +1,64 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package log
import (
"time"
)
// ObjectVersion object version key/versionId
type ObjectVersion struct {
ObjectName string `json:"objectName"`
VersionID string `json:"versionId,omitempty"`
}
// Args - defines the arguments for the API.
type Args struct {
Bucket string `json:"bucket,omitempty"`
Object string `json:"object,omitempty"`
VersionID string `json:"versionId,omitempty"`
Objects []ObjectVersion `json:"objects,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
}
// Trace - defines the trace.
type Trace struct {
Message string `json:"message,omitempty"`
Source []string `json:"source,omitempty"`
Variables map[string]interface{} `json:"variables,omitempty"`
}
// API - defines the api type and its args.
type API struct {
Name string `json:"name,omitempty"`
}
// Entry - defines fields and values of each log entry.
type Entry struct {
DeploymentID string `json:"deploymentid,omitempty"`
Level string `json:"level"`
LogKind string `json:"errKind"`
Time time.Time `json:"time"`
API *API `json:"api,omitempty"`
RemoteHost string `json:"remotehost,omitempty"`
Host string `json:"host,omitempty"`
RequestID string `json:"requestID,omitempty"`
SessionID string `json:"sessionID,omitempty"`
UserAgent string `json:"userAgent,omitempty"`
Message string `json:"message,omitempty"`
Trace *Trace `json:"errors,omitempty"`
}

117
pkg/logger/reqinfo.go Normal file
View File

@@ -0,0 +1,117 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package logger
import (
"context"
"fmt"
"sync"
"github.com/minio/console/pkg/utils"
)
// KeyVal - appended to ReqInfo.Tags
type KeyVal struct {
Key string
Val interface{}
}
// ObjectVersion object version key/versionId
type ObjectVersion struct {
ObjectName string
VersionID string `json:"VersionId,omitempty"`
}
// ReqInfo stores the request info.
type ReqInfo struct {
RemoteHost string // Client Host/IP
Host string // Node Host/IP
UserAgent string // User Agent
DeploymentID string // x-minio-deployment-id
RequestID string // x-amz-request-id
SessionID string // custom session id
API string // API name - GetObject PutObject NewMultipartUpload etc.
BucketName string `json:",omitempty"` // Bucket name
ObjectName string `json:",omitempty"` // Object name
VersionID string `json:",omitempty"` // corresponding versionID for the object
Objects []ObjectVersion `json:",omitempty"` // Only set during MultiObject delete handler.
AccessKey string // Access Key
tags []KeyVal // Any additional info not accommodated by above fields
sync.RWMutex
}
// GetTags - returns the user defined tags
func (r *ReqInfo) GetTags() []KeyVal {
if r == nil {
return nil
}
r.RLock()
defer r.RUnlock()
return append([]KeyVal(nil), r.tags...)
}
// GetTagsMap - returns the user defined tags in a map structure
func (r *ReqInfo) GetTagsMap() map[string]interface{} {
if r == nil {
return nil
}
r.RLock()
defer r.RUnlock()
m := make(map[string]interface{}, len(r.tags))
for _, t := range r.tags {
m[t.Key] = t.Val
}
return m
}
// SetReqInfo sets ReqInfo in the context.
func SetReqInfo(ctx context.Context, req *ReqInfo) context.Context {
if ctx == nil {
LogIf(context.Background(), fmt.Errorf("context is nil"))
return nil
}
return context.WithValue(ctx, utils.ContextLogKey, req)
}
// GetReqInfo returns ReqInfo if set.
func GetReqInfo(ctx context.Context) *ReqInfo {
if ctx != nil {
r, ok := ctx.Value(utils.ContextLogKey).(*ReqInfo)
if ok {
return r
}
r = &ReqInfo{}
if val, o := ctx.Value(utils.ContextRequestID).(string); o {
r.RequestID = val
}
if val, o := ctx.Value(utils.ContextRequestUserID).(string); o {
r.SessionID = val
}
if val, o := ctx.Value(utils.ContextRequestUserAgent).(string); o {
r.UserAgent = val
}
if val, o := ctx.Value(utils.ContextRequestHost).(string); o {
r.Host = val
}
if val, o := ctx.Value(utils.ContextRequestRemoteAddr).(string); o {
r.RemoteHost = val
}
SetReqInfo(ctx, r)
return r
}
return nil
}

View File

@@ -0,0 +1,226 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package http
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"sync"
"sync/atomic"
"time"
xhttp "github.com/minio/console/pkg/http"
"github.com/minio/console/pkg/logger/target/types"
)
// Timeout for the webhook http call
const webhookCallTimeout = 5 * time.Second
// Config http logger target
type Config struct {
Enabled bool `json:"enabled"`
Name string `json:"name"`
UserAgent string `json:"userAgent"`
Endpoint string `json:"endpoint"`
AuthToken string `json:"authToken"`
ClientCert string `json:"clientCert"`
ClientKey string `json:"clientKey"`
QueueSize int `json:"queueSize"`
Transport http.RoundTripper `json:"-"`
// Custom logger
LogOnce func(ctx context.Context, err error, id interface{}, errKind ...interface{}) `json:"-"`
}
// Target implements logger.Target and sends the json
// format of a log entry to the configured http endpoint.
// An internal buffer of logs is maintained but when the
// buffer is full, new logs are just ignored and an errors
// is returned to the caller.
type Target struct {
status int32
wg sync.WaitGroup
// Channel of log entries
logCh chan interface{}
config Config
}
// Endpoint returns the backend endpoint
func (h *Target) Endpoint() string {
return h.config.Endpoint
}
func (h *Target) String() string {
return h.config.Name
}
// Init validate and initialize the http target
func (h *Target) Init() error {
ctx, cancel := context.WithTimeout(context.Background(), 2*webhookCallTimeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, h.config.Endpoint, strings.NewReader(`{}`))
if err != nil {
return err
}
req.Header.Set(xhttp.ContentType, "application/json")
// Set user-agent to indicate MinIO release
// version to the configured log endpoint
req.Header.Set("User-Agent", h.config.UserAgent)
if h.config.AuthToken != "" {
req.Header.Set("Authorization", h.config.AuthToken)
}
client := http.Client{Transport: h.config.Transport}
resp, err := client.Do(req)
if err != nil {
return err
}
// Drain any response.
xhttp.DrainBody(resp.Body)
if !acceptedResponseStatusCode(resp.StatusCode) {
switch resp.StatusCode {
case http.StatusForbidden:
return fmt.Errorf("%s returned '%s', please check if your auth token is correctly set",
h.config.Endpoint, resp.Status)
}
return fmt.Errorf("%s returned '%s', please check your endpoint configuration",
h.config.Endpoint, resp.Status)
}
h.status = 1
go h.startHTTPLogger()
return nil
}
// Accepted HTTP Status Codes
var acceptedStatusCodeMap = map[int]bool{http.StatusOK: true, http.StatusCreated: true, http.StatusAccepted: true, http.StatusNoContent: true}
func acceptedResponseStatusCode(code int) bool {
return acceptedStatusCodeMap[code]
}
func (h *Target) logEntry(entry interface{}) {
logJSON, err := json.Marshal(&entry)
if err != nil {
return
}
ctx, cancel := context.WithTimeout(context.Background(), webhookCallTimeout)
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
h.config.Endpoint, bytes.NewReader(logJSON))
if err != nil {
h.config.LogOnce(ctx, fmt.Errorf("%s returned '%w', please check your endpoint configuration", h.config.Endpoint, err), h.config.Endpoint)
cancel()
return
}
req.Header.Set(xhttp.ContentType, "application/json")
// Set user-agent to indicate MinIO release
// version to the configured log endpoint
req.Header.Set("User-Agent", h.config.UserAgent)
if h.config.AuthToken != "" {
req.Header.Set("Authorization", h.config.AuthToken)
}
client := http.Client{Transport: h.config.Transport}
resp, err := client.Do(req)
cancel()
if err != nil {
h.config.LogOnce(ctx, fmt.Errorf("%s returned '%w', please check your endpoint configuration", h.config.Endpoint, err), h.config.Endpoint)
return
}
// Drain any response.
xhttp.DrainBody(resp.Body)
if !acceptedResponseStatusCode(resp.StatusCode) {
switch resp.StatusCode {
case http.StatusForbidden:
h.config.LogOnce(ctx, fmt.Errorf("%s returned '%s', please check if your auth token is correctly set", h.config.Endpoint, resp.Status), h.config.Endpoint)
default:
h.config.LogOnce(ctx, fmt.Errorf("%s returned '%s', please check your endpoint configuration", h.config.Endpoint, resp.Status), h.config.Endpoint)
}
}
}
func (h *Target) startHTTPLogger() {
// Create a routine which sends json logs received
// from an internal channel.
go func() {
h.wg.Add(1)
defer h.wg.Done()
for entry := range h.logCh {
h.logEntry(entry)
}
}()
}
// New initializes a new logger target which
// sends log over http to the specified endpoint
func New(config Config) *Target {
h := &Target{
logCh: make(chan interface{}, config.QueueSize),
config: config,
}
return h
}
// Send log message 'e' to http target.
func (h *Target) Send(entry interface{}, errKind string) error {
if atomic.LoadInt32(&h.status) == 0 {
// Channel was closed or used before init.
return nil
}
select {
case h.logCh <- entry:
default:
// log channel is full, do not wait and return
// an errors immediately to the caller
return errors.New("log buffer full")
}
return nil
}
// Cancel - cancels the target
func (h *Target) Cancel() {
if atomic.CompareAndSwapInt32(&h.status, 1, 0) {
close(h.logCh)
}
h.wg.Wait()
}
// Type - returns type of the target
func (h *Target) Type() types.TargetType {
return types.TargetHTTP
}

View File

@@ -0,0 +1,27 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package types
// TargetType indicates type of the target e.g. console, http, kafka
type TargetType uint8
// Constants for target types
const (
_ TargetType = iota
TargetConsole
TargetHTTP
)

151
pkg/logger/targets.go Normal file
View File

@@ -0,0 +1,151 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package logger
import (
"sync"
"sync/atomic"
"github.com/minio/console/pkg/logger/target/http"
"github.com/minio/console/pkg/logger/target/types"
)
// Target is the entity that we will receive
// a single log entry and Send it to the log target
// e.g. Send the log to a http server
type Target interface {
String() string
Endpoint() string
Init() error
Cancel()
Send(entry interface{}, errKind string) error
Type() types.TargetType
}
var (
// swapMu must be held while reading slice info or swapping targets or auditTargets.
swapMu sync.Mutex
// systemTargets is the set of enabled loggers.
// Must be immutable at all times.
// Can be swapped to another while holding swapMu
systemTargets = []Target{}
// This is always set represent /dev/console target
consoleTgt Target
nTargets int32 // atomic count of len(targets)
)
// SystemTargets returns active targets.
// Returned slice may not be modified in any way.
func SystemTargets() []Target {
if atomic.LoadInt32(&nTargets) == 0 {
// Lock free if none...
return nil
}
swapMu.Lock()
res := systemTargets
swapMu.Unlock()
return res
}
// AuditTargets returns active audit targets.
// Returned slice may not be modified in any way.
func AuditTargets() []Target {
if atomic.LoadInt32(&nAuditTargets) == 0 {
// Lock free if none...
return nil
}
swapMu.Lock()
res := auditTargets
swapMu.Unlock()
return res
}
// auditTargets is the list of enabled audit loggers
// Must be immutable at all times.
// Can be swapped to another while holding swapMu
var (
auditTargets = []Target{}
nAuditTargets int32 // atomic count of len(auditTargets)
)
func cancelAllSystemTargets() {
for _, tgt := range systemTargets {
tgt.Cancel()
}
}
func initSystemTargets(cfgMap map[string]http.Config) (tgts []Target, err error) {
for _, l := range cfgMap {
if l.Enabled {
t := http.New(l)
if err = t.Init(); err != nil {
return tgts, err
}
tgts = append(tgts, t)
}
}
return tgts, err
}
// UpdateSystemTargets swaps targets with newly loaded ones from the cfg
func UpdateSystemTargets(cfg Config) error {
updated, err := initSystemTargets(cfg.HTTP)
if err != nil {
return err
}
swapMu.Lock()
for _, tgt := range systemTargets {
// Preserve console target when dynamically updating
// other HTTP targets, console target is always present.
if tgt.Type() == types.TargetConsole {
updated = append(updated, tgt)
break
}
}
atomic.StoreInt32(&nTargets, int32(len(updated)))
cancelAllSystemTargets() // cancel running targets
systemTargets = updated
swapMu.Unlock()
return nil
}
func cancelAuditTargetType(t types.TargetType) {
for _, tgt := range auditTargets {
if tgt.Type() == t {
tgt.Cancel()
}
}
}
// UpdateAuditWebhookTargets swaps audit webhook targets with newly loaded ones from the cfg
func UpdateAuditWebhookTargets(cfg Config) error {
updated, err := initSystemTargets(cfg.AuditWebhook)
if err != nil {
return err
}
swapMu.Lock()
atomic.StoreInt32(&nAuditTargets, int32(len(updated)))
cancelAuditTargetType(types.TargetHTTP) // cancel running targets
auditTargets = updated
swapMu.Unlock()
return nil
}

60
pkg/logger/utils.go Normal file
View File

@@ -0,0 +1,60 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package logger
import (
"fmt"
"regexp"
"runtime"
"github.com/minio/console/pkg/logger/color"
)
var ansiRE = regexp.MustCompile("(\x1b[^m]*m)")
// Print ANSI Control escape
func ansiEscape(format string, args ...interface{}) {
Esc := "\x1b"
fmt.Printf("%s%s", Esc, fmt.Sprintf(format, args...))
}
func ansiMoveRight(n int) {
if runtime.GOOS == "windows" {
return
}
if color.IsTerminal() {
ansiEscape("[%dC", n)
}
}
func ansiSaveAttributes() {
if runtime.GOOS == "windows" {
return
}
if color.IsTerminal() {
ansiEscape("7")
}
}
func ansiRestoreAttributes() {
if runtime.GOOS == "windows" {
return
}
if color.IsTerminal() {
ansiEscape("8")
}
}

View File

@@ -24,7 +24,7 @@ import (
"fmt"
"log"
"github.com/minio/console/pkg/utils"
"github.com/minio/console/pkg/http"
"github.com/minio/pkg/licverifier"
@@ -34,7 +34,7 @@ import (
"github.com/tidwall/gjson"
)
func LoginWithMFA(client utils.HTTPClientI, username, mfaToken, otp string) (*LoginResp, error) {
func LoginWithMFA(client http.ClientI, username, mfaToken, otp string) (*LoginResp, error) {
mfaLoginReq := MfaReq{Username: username, OTP: otp, Token: mfaToken}
resp, err := subnetPostReq(client, subnetMFAURL(), mfaLoginReq, nil)
if err != nil {
@@ -47,7 +47,7 @@ func LoginWithMFA(client utils.HTTPClientI, username, mfaToken, otp string) (*Lo
return nil, errors.New("access token not found in response")
}
func Login(client utils.HTTPClientI, username, password string) (*LoginResp, error) {
func Login(client http.ClientI, username, password string) (*LoginResp, error) {
loginReq := map[string]string{
"username": username,
"password": password,
@@ -71,7 +71,7 @@ func Login(client utils.HTTPClientI, username, password string) (*LoginResp, err
return nil, errors.New("access token not found in response")
}
func GetOrganizations(client utils.HTTPClientI, token string) ([]*models.SubnetOrganization, error) {
func GetOrganizations(client http.ClientI, token string) ([]*models.SubnetOrganization, error) {
headers := subnetAuthHeaders(token)
respStr, err := subnetGetReq(client, subnetOrgsURL(), headers)
if err != nil {
@@ -90,7 +90,7 @@ type LicenseTokenConfig struct {
Proxy string
}
func Register(client utils.HTTPClientI, admInfo madmin.InfoMessage, apiKey, token, accountID string) (*LicenseTokenConfig, error) {
func Register(client http.ClientI, admInfo madmin.InfoMessage, apiKey, token, accountID string) (*LicenseTokenConfig, error) {
var headers map[string]string
regInfo := GetClusterRegInfo(admInfo)
regURL := subnetRegisterURL()
@@ -128,7 +128,7 @@ func Register(client utils.HTTPClientI, admInfo madmin.InfoMessage, apiKey, toke
const publicKey = "/downloads/license-pubkey.pem"
// downloadSubnetPublicKey will download the current subnet public key.
func downloadSubnetPublicKey(client utils.HTTPClientI) (string, error) {
func downloadSubnetPublicKey(client http.ClientI) (string, error) {
// Get the public key directly from Subnet
url := fmt.Sprintf("%s%s", subnetBaseURL(), publicKey)
resp, err := client.Get(url)
@@ -145,7 +145,7 @@ func downloadSubnetPublicKey(client utils.HTTPClientI) (string, error) {
}
// ParseLicense parses the license with the bundle public key and return it's information
func ParseLicense(client utils.HTTPClientI, license string) (*licverifier.LicenseInfo, error) {
func ParseLicense(client http.ClientI, license string) (*licverifier.LicenseInfo, error) {
var publicKeys []string
subnetPubKey, err := downloadSubnetPublicKey(client)

View File

@@ -25,7 +25,7 @@ import (
"io/ioutil"
"net/http"
"github.com/minio/console/pkg/utils"
xhttp "github.com/minio/console/pkg/http"
"github.com/minio/madmin-go"
mc "github.com/minio/mc/cmd"
@@ -69,11 +69,11 @@ func subnetAuthHeaders(authToken string) map[string]string {
return map[string]string{"Authorization": "Bearer " + authToken}
}
func httpDo(client utils.HTTPClientI, req *http.Request) (*http.Response, error) {
func httpDo(client xhttp.ClientI, req *http.Request) (*http.Response, error) {
return client.Do(req)
}
func subnetReqDo(client utils.HTTPClientI, r *http.Request, headers map[string]string) (string, error) {
func subnetReqDo(client xhttp.ClientI, r *http.Request, headers map[string]string) (string, error) {
for k, v := range headers {
r.Header.Add(k, v)
}
@@ -98,10 +98,10 @@ func subnetReqDo(client utils.HTTPClientI, r *http.Request, headers map[string]s
if resp.StatusCode == http.StatusOK {
return respStr, nil
}
return respStr, fmt.Errorf("Request failed with code %d and error: %s", resp.StatusCode, respStr)
return respStr, fmt.Errorf("Request failed with code %d and errors: %s", resp.StatusCode, respStr)
}
func subnetGetReq(client utils.HTTPClientI, reqURL string, headers map[string]string) (string, error) {
func subnetGetReq(client xhttp.ClientI, reqURL string, headers map[string]string) (string, error) {
r, e := http.NewRequest(http.MethodGet, reqURL, nil)
if e != nil {
return "", e
@@ -109,7 +109,7 @@ func subnetGetReq(client utils.HTTPClientI, reqURL string, headers map[string]st
return subnetReqDo(client, r, headers)
}
func subnetPostReq(client utils.HTTPClientI, reqURL string, payload interface{}, headers map[string]string) (string, error) {
func subnetPostReq(client xhttp.ClientI, reqURL string, payload interface{}, headers map[string]string) (string, error) {
body, e := json.Marshal(payload)
if e != nil {
return "", e

39
pkg/utils/utils.go Normal file
View File

@@ -0,0 +1,39 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package utils
import "github.com/google/uuid"
// NewUUID - get a random UUID.
func NewUUID() (string, error) {
u, err := uuid.NewRandom()
if err != nil {
return "", err
}
return u.String(), nil
}
// Key used for Get/SetReqInfo
type key string
const ContextLogKey = key("console-log")
const ContextRequestID = key("request-id")
const ContextRequestUserID = key("request-user-id")
const ContextRequestUserAgent = key("request-user-agent")
const ContextRequestHost = key("request-host")
const ContextRequestRemoteAddr = key("request-remote-addr")
const ContextAuditKey = key("request-audit-entry")

View File

@@ -21,6 +21,8 @@ import (
"fmt"
"io/ioutil"
"regexp"
"github.com/minio/console/pkg/http"
)
var (
@@ -28,7 +30,7 @@ var (
)
// getLatestMinIOImage returns the latest docker image for MinIO if found on the internet
func GetLatestMinIOImage(client HTTPClientI) (*string, error) {
func GetLatestMinIOImage(client http.ClientI) (*string, error) {
resp, err := client.Get("https://dl.min.io/server/minio/release/linux-amd64/")
if err != nil {
return nil, err

View File

@@ -1,121 +1,120 @@
{
"files": {
"main.css": "./static/css/main.90d417ae.css",
"main.js": "./static/js/main.eec275cb.js",
"static/js/2483.48dd183a.chunk.js": "./static/js/2483.48dd183a.chunk.js",
"main.js": "./static/js/main.7be4b5f3.js",
"static/js/2483.ebdbad2e.chunk.js": "./static/js/2483.ebdbad2e.chunk.js",
"static/js/6914.c9671304.chunk.js": "./static/js/6914.c9671304.chunk.js",
"static/js/4209.b2656735.chunk.js": "./static/js/4209.b2656735.chunk.js",
"static/js/1829.3a638c7d.chunk.js": "./static/js/1829.3a638c7d.chunk.js",
"static/js/4209.741c5f3e.chunk.js": "./static/js/4209.741c5f3e.chunk.js",
"static/js/1829.0ed9532d.chunk.js": "./static/js/1829.0ed9532d.chunk.js",
"static/js/4455.4ba90afa.chunk.js": "./static/js/4455.4ba90afa.chunk.js",
"static/js/5088.dae21c0f.chunk.js": "./static/js/5088.dae21c0f.chunk.js",
"static/js/5088.07fbe469.chunk.js": "./static/js/5088.07fbe469.chunk.js",
"static/js/5140.e9043b63.chunk.js": "./static/js/5140.e9043b63.chunk.js",
"static/js/5646.e4b3b3e9.chunk.js": "./static/js/5646.e4b3b3e9.chunk.js",
"static/js/5646.df015c08.chunk.js": "./static/js/5646.df015c08.chunk.js",
"static/js/3176.43953acc.chunk.js": "./static/js/3176.43953acc.chunk.js",
"static/js/6137.c0b24aaa.chunk.js": "./static/js/6137.c0b24aaa.chunk.js",
"static/js/7045.ca5a5aae.chunk.js": "./static/js/7045.ca5a5aae.chunk.js",
"static/js/9251.be8506b4.chunk.js": "./static/js/9251.be8506b4.chunk.js",
"static/js/2338.8d87570c.chunk.js": "./static/js/2338.8d87570c.chunk.js",
"static/js/4335.44360980.chunk.js": "./static/js/4335.44360980.chunk.js",
"static/js/3061.c71568b9.chunk.js": "./static/js/3061.c71568b9.chunk.js",
"static/js/6763.f0f3ae01.chunk.js": "./static/js/6763.f0f3ae01.chunk.js",
"static/js/3543.2a18a6b9.chunk.js": "./static/js/3543.2a18a6b9.chunk.js",
"static/js/4061.44b5ee07.chunk.js": "./static/js/4061.44b5ee07.chunk.js",
"static/js/2249.eff0718f.chunk.js": "./static/js/2249.eff0718f.chunk.js",
"static/js/9251.39470f5c.chunk.js": "./static/js/9251.39470f5c.chunk.js",
"static/js/2338.b996d7f6.chunk.js": "./static/js/2338.b996d7f6.chunk.js",
"static/js/4335.48b6eb71.chunk.js": "./static/js/4335.48b6eb71.chunk.js",
"static/js/3061.fdf907c5.chunk.js": "./static/js/3061.fdf907c5.chunk.js",
"static/js/6763.8e2c073b.chunk.js": "./static/js/6763.8e2c073b.chunk.js",
"static/js/3543.7da7dc0a.chunk.js": "./static/js/3543.7da7dc0a.chunk.js",
"static/js/4061.9a8a13eb.chunk.js": "./static/js/4061.9a8a13eb.chunk.js",
"static/js/2249.6f8e7e92.chunk.js": "./static/js/2249.6f8e7e92.chunk.js",
"static/js/9611.c217768e.chunk.js": "./static/js/9611.c217768e.chunk.js",
"static/js/2637.6d2abbec.chunk.js": "./static/js/2637.6d2abbec.chunk.js",
"static/css/380.8313f811.chunk.css": "./static/css/380.8313f811.chunk.css",
"static/js/380.78d0d6a2.chunk.js": "./static/js/380.78d0d6a2.chunk.js",
"static/js/5926.4b729c3b.chunk.js": "./static/js/5926.4b729c3b.chunk.js",
"static/js/2637.02eefe4a.chunk.js": "./static/js/2637.02eefe4a.chunk.js",
"static/css/380.04346438.chunk.css": "./static/css/380.04346438.chunk.css",
"static/js/380.8fcb1392.chunk.js": "./static/js/380.8fcb1392.chunk.js",
"static/js/5926.31770a21.chunk.js": "./static/js/5926.31770a21.chunk.js",
"static/js/701.52180c55.chunk.js": "./static/js/701.52180c55.chunk.js",
"static/js/7821.a2c85c06.chunk.js": "./static/js/7821.a2c85c06.chunk.js",
"static/css/2080.8313f811.chunk.css": "./static/css/2080.8313f811.chunk.css",
"static/js/2080.5b870317.chunk.js": "./static/js/2080.5b870317.chunk.js",
"static/js/6747.a78bbd22.chunk.js": "./static/js/6747.a78bbd22.chunk.js",
"static/css/9033.8313f811.chunk.css": "./static/css/9033.8313f811.chunk.css",
"static/js/9033.b95c6d4a.chunk.js": "./static/js/9033.b95c6d4a.chunk.js",
"static/css/3368.8313f811.chunk.css": "./static/css/3368.8313f811.chunk.css",
"static/js/3368.27a61e61.chunk.js": "./static/js/3368.27a61e61.chunk.js",
"static/css/3688.8313f811.chunk.css": "./static/css/3688.8313f811.chunk.css",
"static/js/3688.fa756e3b.chunk.js": "./static/js/3688.fa756e3b.chunk.js",
"static/js/2555.cd5bfa20.chunk.js": "./static/js/2555.cd5bfa20.chunk.js",
"static/js/7585.78d525ce.chunk.js": "./static/js/7585.78d525ce.chunk.js",
"static/js/1836.86b53328.chunk.js": "./static/js/1836.86b53328.chunk.js",
"static/js/4653.89f5d861.chunk.js": "./static/js/4653.89f5d861.chunk.js",
"static/js/4219.c24a76ef.chunk.js": "./static/js/4219.c24a76ef.chunk.js",
"static/js/8626.896fa8ac.chunk.js": "./static/js/8626.896fa8ac.chunk.js",
"static/js/736.40b3a390.chunk.js": "./static/js/736.40b3a390.chunk.js",
"static/js/6577.d48f6fc0.chunk.js": "./static/js/6577.d48f6fc0.chunk.js",
"static/js/9561.46bb02ad.chunk.js": "./static/js/9561.46bb02ad.chunk.js",
"static/js/4394.05e77f06.chunk.js": "./static/js/4394.05e77f06.chunk.js",
"static/css/2080.04346438.chunk.css": "./static/css/2080.04346438.chunk.css",
"static/js/2080.07b74e77.chunk.js": "./static/js/2080.07b74e77.chunk.js",
"static/js/4352.ecb9b923.chunk.js": "./static/js/4352.ecb9b923.chunk.js",
"static/css/9033.04346438.chunk.css": "./static/css/9033.04346438.chunk.css",
"static/js/9033.e6865183.chunk.js": "./static/js/9033.e6865183.chunk.js",
"static/css/6633.04346438.chunk.css": "./static/css/6633.04346438.chunk.css",
"static/js/6633.2d6d234a.chunk.js": "./static/js/6633.2d6d234a.chunk.js",
"static/css/6859.04346438.chunk.css": "./static/css/6859.04346438.chunk.css",
"static/js/6859.e8e4a97d.chunk.js": "./static/js/6859.e8e4a97d.chunk.js",
"static/js/2555.f9ba93a3.chunk.js": "./static/js/2555.f9ba93a3.chunk.js",
"static/js/7585.a08ac645.chunk.js": "./static/js/7585.a08ac645.chunk.js",
"static/js/1836.b42dfba9.chunk.js": "./static/js/1836.b42dfba9.chunk.js",
"static/js/4653.f372ea27.chunk.js": "./static/js/4653.f372ea27.chunk.js",
"static/js/4219.e5abf39a.chunk.js": "./static/js/4219.e5abf39a.chunk.js",
"static/js/8626.cae557ae.chunk.js": "./static/js/8626.cae557ae.chunk.js",
"static/js/736.853a950a.chunk.js": "./static/js/736.853a950a.chunk.js",
"static/js/6577.c8b36701.chunk.js": "./static/js/6577.c8b36701.chunk.js",
"static/js/9561.dbb650ca.chunk.js": "./static/js/9561.dbb650ca.chunk.js",
"static/js/4394.132a2087.chunk.js": "./static/js/4394.132a2087.chunk.js",
"static/js/4781.785d14ba.chunk.js": "./static/js/4781.785d14ba.chunk.js",
"static/js/9478.7c40d91e.chunk.js": "./static/js/9478.7c40d91e.chunk.js",
"static/js/7164.5542a849.chunk.js": "./static/js/7164.5542a849.chunk.js",
"static/js/4414.d9f98702.chunk.js": "./static/js/4414.d9f98702.chunk.js",
"static/js/7798.07093714.chunk.js": "./static/js/7798.07093714.chunk.js",
"static/js/8833.60296031.chunk.js": "./static/js/8833.60296031.chunk.js",
"static/js/471.244e997d.chunk.js": "./static/js/471.244e997d.chunk.js",
"static/js/483.a8ef0ad5.chunk.js": "./static/js/483.a8ef0ad5.chunk.js",
"static/js/9467.5fb2ba42.chunk.js": "./static/js/9467.5fb2ba42.chunk.js",
"static/js/6895.10b5757c.chunk.js": "./static/js/6895.10b5757c.chunk.js",
"static/js/6233.c0a85e71.chunk.js": "./static/js/6233.c0a85e71.chunk.js",
"static/js/5588.fa4e23ec.chunk.js": "./static/js/5588.fa4e23ec.chunk.js",
"static/js/4133.1b71a3ec.chunk.js": "./static/js/4133.1b71a3ec.chunk.js",
"static/css/1955.8313f811.chunk.css": "./static/css/1955.8313f811.chunk.css",
"static/js/1955.fcaedc3e.chunk.js": "./static/js/1955.fcaedc3e.chunk.js",
"static/js/3956.0b36ab08.chunk.js": "./static/js/3956.0b36ab08.chunk.js",
"static/js/4414.95bb9b72.chunk.js": "./static/js/4414.95bb9b72.chunk.js",
"static/js/7798.36139559.chunk.js": "./static/js/7798.36139559.chunk.js",
"static/js/8833.8c3d2d1a.chunk.js": "./static/js/8833.8c3d2d1a.chunk.js",
"static/js/471.cc100d23.chunk.js": "./static/js/471.cc100d23.chunk.js",
"static/js/483.14f274ee.chunk.js": "./static/js/483.14f274ee.chunk.js",
"static/js/9467.22552611.chunk.js": "./static/js/9467.22552611.chunk.js",
"static/js/6895.403b3023.chunk.js": "./static/js/6895.403b3023.chunk.js",
"static/js/6233.dc9e7015.chunk.js": "./static/js/6233.dc9e7015.chunk.js",
"static/js/5588.358d372f.chunk.js": "./static/js/5588.358d372f.chunk.js",
"static/js/4133.d301b843.chunk.js": "./static/js/4133.d301b843.chunk.js",
"static/css/1955.04346438.chunk.css": "./static/css/1955.04346438.chunk.css",
"static/js/1955.d60ad4bd.chunk.js": "./static/js/1955.d60ad4bd.chunk.js",
"static/js/3956.bc3ae9b9.chunk.js": "./static/js/3956.bc3ae9b9.chunk.js",
"static/js/8771.475684ba.chunk.js": "./static/js/8771.475684ba.chunk.js",
"static/js/9076.a6606f2e.chunk.js": "./static/js/9076.a6606f2e.chunk.js",
"static/js/9221.ba80075c.chunk.js": "./static/js/9221.ba80075c.chunk.js",
"static/js/8896.09fd2a0c.chunk.js": "./static/js/8896.09fd2a0c.chunk.js",
"static/js/7413.73b72a2b.chunk.js": "./static/js/7413.73b72a2b.chunk.js",
"static/js/9076.596078c8.chunk.js": "./static/js/9076.596078c8.chunk.js",
"static/js/9221.afc1d068.chunk.js": "./static/js/9221.afc1d068.chunk.js",
"static/js/8896.ea205ba2.chunk.js": "./static/js/8896.ea205ba2.chunk.js",
"static/js/9134.b0935ef3.chunk.js": "./static/js/9134.b0935ef3.chunk.js",
"static/css/8138.8313f811.chunk.css": "./static/css/8138.8313f811.chunk.css",
"static/js/8138.e9fa48a9.chunk.js": "./static/js/8138.e9fa48a9.chunk.js",
"static/js/1030.4a654568.chunk.js": "./static/js/1030.4a654568.chunk.js",
"static/css/8138.04346438.chunk.css": "./static/css/8138.04346438.chunk.css",
"static/js/8138.f97957dc.chunk.js": "./static/js/8138.f97957dc.chunk.js",
"static/js/1030.986c2667.chunk.js": "./static/js/1030.986c2667.chunk.js",
"static/js/9145.363b2352.chunk.js": "./static/js/9145.363b2352.chunk.js",
"static/js/1379.0bfc0b60.chunk.js": "./static/js/1379.0bfc0b60.chunk.js",
"static/js/1501.c46671fd.chunk.js": "./static/js/1501.c46671fd.chunk.js",
"static/js/9605.c3d4a4cf.chunk.js": "./static/js/9605.c3d4a4cf.chunk.js",
"static/js/426.e738683c.chunk.js": "./static/js/426.e738683c.chunk.js",
"static/js/1379.7cd1ce08.chunk.js": "./static/js/1379.7cd1ce08.chunk.js",
"static/js/1501.44151d90.chunk.js": "./static/js/1501.44151d90.chunk.js",
"static/js/9605.1542e970.chunk.js": "./static/js/9605.1542e970.chunk.js",
"static/js/426.1a73abd5.chunk.js": "./static/js/426.1a73abd5.chunk.js",
"static/js/2878.fca6e2cf.chunk.js": "./static/js/2878.fca6e2cf.chunk.js",
"static/js/8495.bdd215dc.chunk.js": "./static/js/8495.bdd215dc.chunk.js",
"static/js/4934.4a573b0b.chunk.js": "./static/js/4934.4a573b0b.chunk.js",
"static/js/3518.e1923a22.chunk.js": "./static/js/3518.e1923a22.chunk.js",
"static/js/7021.ea551e6b.chunk.js": "./static/js/7021.ea551e6b.chunk.js",
"static/js/2684.569c8172.chunk.js": "./static/js/2684.569c8172.chunk.js",
"static/js/6683.53f69e13.chunk.js": "./static/js/6683.53f69e13.chunk.js",
"static/js/8350.031612d5.chunk.js": "./static/js/8350.031612d5.chunk.js",
"static/js/7021.8869219f.chunk.js": "./static/js/7021.8869219f.chunk.js",
"static/js/2684.8bdcdf4b.chunk.js": "./static/js/2684.8bdcdf4b.chunk.js",
"static/js/6683.dc4f1821.chunk.js": "./static/js/6683.dc4f1821.chunk.js",
"static/js/8350.8e2fd461.chunk.js": "./static/js/8350.8e2fd461.chunk.js",
"static/js/2676.bd3d9df3.chunk.js": "./static/js/2676.bd3d9df3.chunk.js",
"static/js/9449.83d73a19.chunk.js": "./static/js/9449.83d73a19.chunk.js",
"static/js/7659.5154337d.chunk.js": "./static/js/7659.5154337d.chunk.js",
"static/js/9968.a7a1674d.chunk.js": "./static/js/9968.a7a1674d.chunk.js",
"static/js/2180.c83301fc.chunk.js": "./static/js/2180.c83301fc.chunk.js",
"static/js/9449.d160d883.chunk.js": "./static/js/9449.d160d883.chunk.js",
"static/js/7659.845e0cad.chunk.js": "./static/js/7659.845e0cad.chunk.js",
"static/js/9968.e41064fb.chunk.js": "./static/js/9968.e41064fb.chunk.js",
"static/js/2180.ec9a5c77.chunk.js": "./static/js/2180.ec9a5c77.chunk.js",
"static/js/8253.964026c0.chunk.js": "./static/js/8253.964026c0.chunk.js",
"static/js/3328.da1cf1c8.chunk.js": "./static/js/3328.da1cf1c8.chunk.js",
"static/js/3328.c97cfa34.chunk.js": "./static/js/3328.c97cfa34.chunk.js",
"static/js/1440.74dce637.chunk.js": "./static/js/1440.74dce637.chunk.js",
"static/js/2512.acfc57ce.chunk.js": "./static/js/2512.acfc57ce.chunk.js",
"static/js/51.63259724.chunk.js": "./static/js/51.63259724.chunk.js",
"static/js/711.5cec9776.chunk.js": "./static/js/711.5cec9776.chunk.js",
"static/js/6901.53c6aef7.chunk.js": "./static/js/6901.53c6aef7.chunk.js",
"static/js/2185.95c76a1b.chunk.js": "./static/js/2185.95c76a1b.chunk.js",
"static/js/312.d183c8e0.chunk.js": "./static/js/312.d183c8e0.chunk.js",
"static/js/2112.48f0caa6.chunk.js": "./static/js/2112.48f0caa6.chunk.js",
"static/js/4619.f7970b8a.chunk.js": "./static/js/4619.f7970b8a.chunk.js",
"static/js/8990.18a36cac.chunk.js": "./static/js/8990.18a36cac.chunk.js",
"static/js/8455.0cd71acb.chunk.js": "./static/js/8455.0cd71acb.chunk.js",
"static/css/3631.8313f811.chunk.css": "./static/css/3631.8313f811.chunk.css",
"static/js/3631.64015ba0.chunk.js": "./static/js/3631.64015ba0.chunk.js",
"static/js/2512.a00182cb.chunk.js": "./static/js/2512.a00182cb.chunk.js",
"static/js/51.7a2ef89a.chunk.js": "./static/js/51.7a2ef89a.chunk.js",
"static/js/711.71917357.chunk.js": "./static/js/711.71917357.chunk.js",
"static/js/6901.9f0fcbd5.chunk.js": "./static/js/6901.9f0fcbd5.chunk.js",
"static/js/2185.50d8f062.chunk.js": "./static/js/2185.50d8f062.chunk.js",
"static/js/312.a4c03743.chunk.js": "./static/js/312.a4c03743.chunk.js",
"static/js/2112.4691ccbf.chunk.js": "./static/js/2112.4691ccbf.chunk.js",
"static/js/4619.6dd3ad70.chunk.js": "./static/js/4619.6dd3ad70.chunk.js",
"static/js/8990.181bff03.chunk.js": "./static/js/8990.181bff03.chunk.js",
"static/js/8455.27bfde53.chunk.js": "./static/js/8455.27bfde53.chunk.js",
"static/css/3631.04346438.chunk.css": "./static/css/3631.04346438.chunk.css",
"static/js/3631.5db9f8b6.chunk.js": "./static/js/3631.5db9f8b6.chunk.js",
"static/js/1604.a9d0b62b.chunk.js": "./static/js/1604.a9d0b62b.chunk.js",
"static/js/8391.7c39b52d.chunk.js": "./static/js/8391.7c39b52d.chunk.js",
"static/js/402.0fe11251.chunk.js": "./static/js/402.0fe11251.chunk.js",
"static/js/8391.a94a5da9.chunk.js": "./static/js/8391.a94a5da9.chunk.js",
"static/js/402.087a85de.chunk.js": "./static/js/402.087a85de.chunk.js",
"static/js/1705.5e57fd31.chunk.js": "./static/js/1705.5e57fd31.chunk.js",
"static/js/1581.e5ea40c0.chunk.js": "./static/js/1581.e5ea40c0.chunk.js",
"static/js/455.0218ce38.chunk.js": "./static/js/455.0218ce38.chunk.js",
"static/js/2661.2121f536.chunk.js": "./static/js/2661.2121f536.chunk.js",
"static/js/889.3d385602.chunk.js": "./static/js/889.3d385602.chunk.js",
"static/js/9088.e252c094.chunk.js": "./static/js/9088.e252c094.chunk.js",
"static/js/247.88de16aa.chunk.js": "./static/js/247.88de16aa.chunk.js",
"static/js/2763.44403c2a.chunk.js": "./static/js/2763.44403c2a.chunk.js",
"static/js/1581.cbc3b85a.chunk.js": "./static/js/1581.cbc3b85a.chunk.js",
"static/js/455.dd2c1a2c.chunk.js": "./static/js/455.dd2c1a2c.chunk.js",
"static/js/2661.ffb402c6.chunk.js": "./static/js/2661.ffb402c6.chunk.js",
"static/js/889.34a82de2.chunk.js": "./static/js/889.34a82de2.chunk.js",
"static/js/9088.edd377c6.chunk.js": "./static/js/9088.edd377c6.chunk.js",
"static/js/247.10687506.chunk.js": "./static/js/247.10687506.chunk.js",
"static/js/2763.247d377c.chunk.js": "./static/js/2763.247d377c.chunk.js",
"static/js/5171.2cf876b1.chunk.js": "./static/js/5171.2cf876b1.chunk.js",
"static/js/2426.172b5361.chunk.js": "./static/js/2426.172b5361.chunk.js",
"static/js/5561.c5000912.chunk.js": "./static/js/5561.c5000912.chunk.js",
@@ -144,126 +143,125 @@
"static/js/9785.7ccf0212.chunk.js": "./static/js/9785.7ccf0212.chunk.js",
"static/js/8735.52726eac.chunk.js": "./static/js/8735.52726eac.chunk.js",
"static/js/63.830fd6fc.chunk.js": "./static/js/63.830fd6fc.chunk.js",
"static/js/2983.66cf0ad4.chunk.js": "./static/js/2983.66cf0ad4.chunk.js",
"static/js/5289.8c388542.chunk.js": "./static/js/5289.8c388542.chunk.js",
"static/js/5026.0b30f6e2.chunk.js": "./static/js/5026.0b30f6e2.chunk.js",
"static/js/2983.3e4b1a45.chunk.js": "./static/js/2983.3e4b1a45.chunk.js",
"static/js/5289.1434b8bc.chunk.js": "./static/js/5289.1434b8bc.chunk.js",
"static/js/5026.ebd99276.chunk.js": "./static/js/5026.ebd99276.chunk.js",
"index.html": "./index.html",
"main.90d417ae.css.map": "./static/css/main.90d417ae.css.map",
"main.eec275cb.js.map": "./static/js/main.eec275cb.js.map",
"2483.48dd183a.chunk.js.map": "./static/js/2483.48dd183a.chunk.js.map",
"main.7be4b5f3.js.map": "./static/js/main.7be4b5f3.js.map",
"2483.ebdbad2e.chunk.js.map": "./static/js/2483.ebdbad2e.chunk.js.map",
"6914.c9671304.chunk.js.map": "./static/js/6914.c9671304.chunk.js.map",
"4209.b2656735.chunk.js.map": "./static/js/4209.b2656735.chunk.js.map",
"1829.3a638c7d.chunk.js.map": "./static/js/1829.3a638c7d.chunk.js.map",
"4209.741c5f3e.chunk.js.map": "./static/js/4209.741c5f3e.chunk.js.map",
"1829.0ed9532d.chunk.js.map": "./static/js/1829.0ed9532d.chunk.js.map",
"4455.4ba90afa.chunk.js.map": "./static/js/4455.4ba90afa.chunk.js.map",
"5088.dae21c0f.chunk.js.map": "./static/js/5088.dae21c0f.chunk.js.map",
"5088.07fbe469.chunk.js.map": "./static/js/5088.07fbe469.chunk.js.map",
"5140.e9043b63.chunk.js.map": "./static/js/5140.e9043b63.chunk.js.map",
"5646.e4b3b3e9.chunk.js.map": "./static/js/5646.e4b3b3e9.chunk.js.map",
"5646.df015c08.chunk.js.map": "./static/js/5646.df015c08.chunk.js.map",
"3176.43953acc.chunk.js.map": "./static/js/3176.43953acc.chunk.js.map",
"6137.c0b24aaa.chunk.js.map": "./static/js/6137.c0b24aaa.chunk.js.map",
"7045.ca5a5aae.chunk.js.map": "./static/js/7045.ca5a5aae.chunk.js.map",
"9251.be8506b4.chunk.js.map": "./static/js/9251.be8506b4.chunk.js.map",
"2338.8d87570c.chunk.js.map": "./static/js/2338.8d87570c.chunk.js.map",
"4335.44360980.chunk.js.map": "./static/js/4335.44360980.chunk.js.map",
"3061.c71568b9.chunk.js.map": "./static/js/3061.c71568b9.chunk.js.map",
"6763.f0f3ae01.chunk.js.map": "./static/js/6763.f0f3ae01.chunk.js.map",
"3543.2a18a6b9.chunk.js.map": "./static/js/3543.2a18a6b9.chunk.js.map",
"4061.44b5ee07.chunk.js.map": "./static/js/4061.44b5ee07.chunk.js.map",
"2249.eff0718f.chunk.js.map": "./static/js/2249.eff0718f.chunk.js.map",
"9251.39470f5c.chunk.js.map": "./static/js/9251.39470f5c.chunk.js.map",
"2338.b996d7f6.chunk.js.map": "./static/js/2338.b996d7f6.chunk.js.map",
"4335.48b6eb71.chunk.js.map": "./static/js/4335.48b6eb71.chunk.js.map",
"3061.fdf907c5.chunk.js.map": "./static/js/3061.fdf907c5.chunk.js.map",
"6763.8e2c073b.chunk.js.map": "./static/js/6763.8e2c073b.chunk.js.map",
"3543.7da7dc0a.chunk.js.map": "./static/js/3543.7da7dc0a.chunk.js.map",
"4061.9a8a13eb.chunk.js.map": "./static/js/4061.9a8a13eb.chunk.js.map",
"2249.6f8e7e92.chunk.js.map": "./static/js/2249.6f8e7e92.chunk.js.map",
"9611.c217768e.chunk.js.map": "./static/js/9611.c217768e.chunk.js.map",
"2637.6d2abbec.chunk.js.map": "./static/js/2637.6d2abbec.chunk.js.map",
"380.8313f811.chunk.css.map": "./static/css/380.8313f811.chunk.css.map",
"380.78d0d6a2.chunk.js.map": "./static/js/380.78d0d6a2.chunk.js.map",
"5926.4b729c3b.chunk.js.map": "./static/js/5926.4b729c3b.chunk.js.map",
"2637.02eefe4a.chunk.js.map": "./static/js/2637.02eefe4a.chunk.js.map",
"380.04346438.chunk.css.map": "./static/css/380.04346438.chunk.css.map",
"380.8fcb1392.chunk.js.map": "./static/js/380.8fcb1392.chunk.js.map",
"5926.31770a21.chunk.js.map": "./static/js/5926.31770a21.chunk.js.map",
"701.52180c55.chunk.js.map": "./static/js/701.52180c55.chunk.js.map",
"7821.a2c85c06.chunk.js.map": "./static/js/7821.a2c85c06.chunk.js.map",
"2080.8313f811.chunk.css.map": "./static/css/2080.8313f811.chunk.css.map",
"2080.5b870317.chunk.js.map": "./static/js/2080.5b870317.chunk.js.map",
"6747.a78bbd22.chunk.js.map": "./static/js/6747.a78bbd22.chunk.js.map",
"9033.8313f811.chunk.css.map": "./static/css/9033.8313f811.chunk.css.map",
"9033.b95c6d4a.chunk.js.map": "./static/js/9033.b95c6d4a.chunk.js.map",
"3368.8313f811.chunk.css.map": "./static/css/3368.8313f811.chunk.css.map",
"3368.27a61e61.chunk.js.map": "./static/js/3368.27a61e61.chunk.js.map",
"3688.8313f811.chunk.css.map": "./static/css/3688.8313f811.chunk.css.map",
"3688.fa756e3b.chunk.js.map": "./static/js/3688.fa756e3b.chunk.js.map",
"2555.cd5bfa20.chunk.js.map": "./static/js/2555.cd5bfa20.chunk.js.map",
"7585.78d525ce.chunk.js.map": "./static/js/7585.78d525ce.chunk.js.map",
"1836.86b53328.chunk.js.map": "./static/js/1836.86b53328.chunk.js.map",
"4653.89f5d861.chunk.js.map": "./static/js/4653.89f5d861.chunk.js.map",
"4219.c24a76ef.chunk.js.map": "./static/js/4219.c24a76ef.chunk.js.map",
"8626.896fa8ac.chunk.js.map": "./static/js/8626.896fa8ac.chunk.js.map",
"736.40b3a390.chunk.js.map": "./static/js/736.40b3a390.chunk.js.map",
"6577.d48f6fc0.chunk.js.map": "./static/js/6577.d48f6fc0.chunk.js.map",
"9561.46bb02ad.chunk.js.map": "./static/js/9561.46bb02ad.chunk.js.map",
"4394.05e77f06.chunk.js.map": "./static/js/4394.05e77f06.chunk.js.map",
"2080.04346438.chunk.css.map": "./static/css/2080.04346438.chunk.css.map",
"2080.07b74e77.chunk.js.map": "./static/js/2080.07b74e77.chunk.js.map",
"4352.ecb9b923.chunk.js.map": "./static/js/4352.ecb9b923.chunk.js.map",
"9033.04346438.chunk.css.map": "./static/css/9033.04346438.chunk.css.map",
"9033.e6865183.chunk.js.map": "./static/js/9033.e6865183.chunk.js.map",
"6633.04346438.chunk.css.map": "./static/css/6633.04346438.chunk.css.map",
"6633.2d6d234a.chunk.js.map": "./static/js/6633.2d6d234a.chunk.js.map",
"6859.04346438.chunk.css.map": "./static/css/6859.04346438.chunk.css.map",
"6859.e8e4a97d.chunk.js.map": "./static/js/6859.e8e4a97d.chunk.js.map",
"2555.f9ba93a3.chunk.js.map": "./static/js/2555.f9ba93a3.chunk.js.map",
"7585.a08ac645.chunk.js.map": "./static/js/7585.a08ac645.chunk.js.map",
"1836.b42dfba9.chunk.js.map": "./static/js/1836.b42dfba9.chunk.js.map",
"4653.f372ea27.chunk.js.map": "./static/js/4653.f372ea27.chunk.js.map",
"4219.e5abf39a.chunk.js.map": "./static/js/4219.e5abf39a.chunk.js.map",
"8626.cae557ae.chunk.js.map": "./static/js/8626.cae557ae.chunk.js.map",
"736.853a950a.chunk.js.map": "./static/js/736.853a950a.chunk.js.map",
"6577.c8b36701.chunk.js.map": "./static/js/6577.c8b36701.chunk.js.map",
"9561.dbb650ca.chunk.js.map": "./static/js/9561.dbb650ca.chunk.js.map",
"4394.132a2087.chunk.js.map": "./static/js/4394.132a2087.chunk.js.map",
"4781.785d14ba.chunk.js.map": "./static/js/4781.785d14ba.chunk.js.map",
"9478.7c40d91e.chunk.js.map": "./static/js/9478.7c40d91e.chunk.js.map",
"7164.5542a849.chunk.js.map": "./static/js/7164.5542a849.chunk.js.map",
"4414.d9f98702.chunk.js.map": "./static/js/4414.d9f98702.chunk.js.map",
"7798.07093714.chunk.js.map": "./static/js/7798.07093714.chunk.js.map",
"8833.60296031.chunk.js.map": "./static/js/8833.60296031.chunk.js.map",
"471.244e997d.chunk.js.map": "./static/js/471.244e997d.chunk.js.map",
"483.a8ef0ad5.chunk.js.map": "./static/js/483.a8ef0ad5.chunk.js.map",
"9467.5fb2ba42.chunk.js.map": "./static/js/9467.5fb2ba42.chunk.js.map",
"6895.10b5757c.chunk.js.map": "./static/js/6895.10b5757c.chunk.js.map",
"6233.c0a85e71.chunk.js.map": "./static/js/6233.c0a85e71.chunk.js.map",
"5588.fa4e23ec.chunk.js.map": "./static/js/5588.fa4e23ec.chunk.js.map",
"4133.1b71a3ec.chunk.js.map": "./static/js/4133.1b71a3ec.chunk.js.map",
"1955.8313f811.chunk.css.map": "./static/css/1955.8313f811.chunk.css.map",
"1955.fcaedc3e.chunk.js.map": "./static/js/1955.fcaedc3e.chunk.js.map",
"3956.0b36ab08.chunk.js.map": "./static/js/3956.0b36ab08.chunk.js.map",
"4414.95bb9b72.chunk.js.map": "./static/js/4414.95bb9b72.chunk.js.map",
"7798.36139559.chunk.js.map": "./static/js/7798.36139559.chunk.js.map",
"8833.8c3d2d1a.chunk.js.map": "./static/js/8833.8c3d2d1a.chunk.js.map",
"471.cc100d23.chunk.js.map": "./static/js/471.cc100d23.chunk.js.map",
"483.14f274ee.chunk.js.map": "./static/js/483.14f274ee.chunk.js.map",
"9467.22552611.chunk.js.map": "./static/js/9467.22552611.chunk.js.map",
"6895.403b3023.chunk.js.map": "./static/js/6895.403b3023.chunk.js.map",
"6233.dc9e7015.chunk.js.map": "./static/js/6233.dc9e7015.chunk.js.map",
"5588.358d372f.chunk.js.map": "./static/js/5588.358d372f.chunk.js.map",
"4133.d301b843.chunk.js.map": "./static/js/4133.d301b843.chunk.js.map",
"1955.04346438.chunk.css.map": "./static/css/1955.04346438.chunk.css.map",
"1955.d60ad4bd.chunk.js.map": "./static/js/1955.d60ad4bd.chunk.js.map",
"3956.bc3ae9b9.chunk.js.map": "./static/js/3956.bc3ae9b9.chunk.js.map",
"8771.475684ba.chunk.js.map": "./static/js/8771.475684ba.chunk.js.map",
"9076.a6606f2e.chunk.js.map": "./static/js/9076.a6606f2e.chunk.js.map",
"9221.ba80075c.chunk.js.map": "./static/js/9221.ba80075c.chunk.js.map",
"8896.09fd2a0c.chunk.js.map": "./static/js/8896.09fd2a0c.chunk.js.map",
"7413.73b72a2b.chunk.js.map": "./static/js/7413.73b72a2b.chunk.js.map",
"9076.596078c8.chunk.js.map": "./static/js/9076.596078c8.chunk.js.map",
"9221.afc1d068.chunk.js.map": "./static/js/9221.afc1d068.chunk.js.map",
"8896.ea205ba2.chunk.js.map": "./static/js/8896.ea205ba2.chunk.js.map",
"9134.b0935ef3.chunk.js.map": "./static/js/9134.b0935ef3.chunk.js.map",
"8138.8313f811.chunk.css.map": "./static/css/8138.8313f811.chunk.css.map",
"8138.e9fa48a9.chunk.js.map": "./static/js/8138.e9fa48a9.chunk.js.map",
"1030.4a654568.chunk.js.map": "./static/js/1030.4a654568.chunk.js.map",
"8138.04346438.chunk.css.map": "./static/css/8138.04346438.chunk.css.map",
"8138.f97957dc.chunk.js.map": "./static/js/8138.f97957dc.chunk.js.map",
"1030.986c2667.chunk.js.map": "./static/js/1030.986c2667.chunk.js.map",
"9145.363b2352.chunk.js.map": "./static/js/9145.363b2352.chunk.js.map",
"1379.0bfc0b60.chunk.js.map": "./static/js/1379.0bfc0b60.chunk.js.map",
"1501.c46671fd.chunk.js.map": "./static/js/1501.c46671fd.chunk.js.map",
"9605.c3d4a4cf.chunk.js.map": "./static/js/9605.c3d4a4cf.chunk.js.map",
"426.e738683c.chunk.js.map": "./static/js/426.e738683c.chunk.js.map",
"1379.7cd1ce08.chunk.js.map": "./static/js/1379.7cd1ce08.chunk.js.map",
"1501.44151d90.chunk.js.map": "./static/js/1501.44151d90.chunk.js.map",
"9605.1542e970.chunk.js.map": "./static/js/9605.1542e970.chunk.js.map",
"426.1a73abd5.chunk.js.map": "./static/js/426.1a73abd5.chunk.js.map",
"2878.fca6e2cf.chunk.js.map": "./static/js/2878.fca6e2cf.chunk.js.map",
"8495.bdd215dc.chunk.js.map": "./static/js/8495.bdd215dc.chunk.js.map",
"4934.4a573b0b.chunk.js.map": "./static/js/4934.4a573b0b.chunk.js.map",
"3518.e1923a22.chunk.js.map": "./static/js/3518.e1923a22.chunk.js.map",
"7021.ea551e6b.chunk.js.map": "./static/js/7021.ea551e6b.chunk.js.map",
"2684.569c8172.chunk.js.map": "./static/js/2684.569c8172.chunk.js.map",
"6683.53f69e13.chunk.js.map": "./static/js/6683.53f69e13.chunk.js.map",
"8350.031612d5.chunk.js.map": "./static/js/8350.031612d5.chunk.js.map",
"7021.8869219f.chunk.js.map": "./static/js/7021.8869219f.chunk.js.map",
"2684.8bdcdf4b.chunk.js.map": "./static/js/2684.8bdcdf4b.chunk.js.map",
"6683.dc4f1821.chunk.js.map": "./static/js/6683.dc4f1821.chunk.js.map",
"8350.8e2fd461.chunk.js.map": "./static/js/8350.8e2fd461.chunk.js.map",
"2676.bd3d9df3.chunk.js.map": "./static/js/2676.bd3d9df3.chunk.js.map",
"9449.83d73a19.chunk.js.map": "./static/js/9449.83d73a19.chunk.js.map",
"7659.5154337d.chunk.js.map": "./static/js/7659.5154337d.chunk.js.map",
"9968.a7a1674d.chunk.js.map": "./static/js/9968.a7a1674d.chunk.js.map",
"2180.c83301fc.chunk.js.map": "./static/js/2180.c83301fc.chunk.js.map",
"9449.d160d883.chunk.js.map": "./static/js/9449.d160d883.chunk.js.map",
"7659.845e0cad.chunk.js.map": "./static/js/7659.845e0cad.chunk.js.map",
"9968.e41064fb.chunk.js.map": "./static/js/9968.e41064fb.chunk.js.map",
"2180.ec9a5c77.chunk.js.map": "./static/js/2180.ec9a5c77.chunk.js.map",
"8253.964026c0.chunk.js.map": "./static/js/8253.964026c0.chunk.js.map",
"3328.da1cf1c8.chunk.js.map": "./static/js/3328.da1cf1c8.chunk.js.map",
"3328.c97cfa34.chunk.js.map": "./static/js/3328.c97cfa34.chunk.js.map",
"1440.74dce637.chunk.js.map": "./static/js/1440.74dce637.chunk.js.map",
"2512.acfc57ce.chunk.js.map": "./static/js/2512.acfc57ce.chunk.js.map",
"51.63259724.chunk.js.map": "./static/js/51.63259724.chunk.js.map",
"711.5cec9776.chunk.js.map": "./static/js/711.5cec9776.chunk.js.map",
"6901.53c6aef7.chunk.js.map": "./static/js/6901.53c6aef7.chunk.js.map",
"2185.95c76a1b.chunk.js.map": "./static/js/2185.95c76a1b.chunk.js.map",
"312.d183c8e0.chunk.js.map": "./static/js/312.d183c8e0.chunk.js.map",
"2112.48f0caa6.chunk.js.map": "./static/js/2112.48f0caa6.chunk.js.map",
"4619.f7970b8a.chunk.js.map": "./static/js/4619.f7970b8a.chunk.js.map",
"8990.18a36cac.chunk.js.map": "./static/js/8990.18a36cac.chunk.js.map",
"8455.0cd71acb.chunk.js.map": "./static/js/8455.0cd71acb.chunk.js.map",
"3631.8313f811.chunk.css.map": "./static/css/3631.8313f811.chunk.css.map",
"3631.64015ba0.chunk.js.map": "./static/js/3631.64015ba0.chunk.js.map",
"2512.a00182cb.chunk.js.map": "./static/js/2512.a00182cb.chunk.js.map",
"51.7a2ef89a.chunk.js.map": "./static/js/51.7a2ef89a.chunk.js.map",
"711.71917357.chunk.js.map": "./static/js/711.71917357.chunk.js.map",
"6901.9f0fcbd5.chunk.js.map": "./static/js/6901.9f0fcbd5.chunk.js.map",
"2185.50d8f062.chunk.js.map": "./static/js/2185.50d8f062.chunk.js.map",
"312.a4c03743.chunk.js.map": "./static/js/312.a4c03743.chunk.js.map",
"2112.4691ccbf.chunk.js.map": "./static/js/2112.4691ccbf.chunk.js.map",
"4619.6dd3ad70.chunk.js.map": "./static/js/4619.6dd3ad70.chunk.js.map",
"8990.181bff03.chunk.js.map": "./static/js/8990.181bff03.chunk.js.map",
"8455.27bfde53.chunk.js.map": "./static/js/8455.27bfde53.chunk.js.map",
"3631.04346438.chunk.css.map": "./static/css/3631.04346438.chunk.css.map",
"3631.5db9f8b6.chunk.js.map": "./static/js/3631.5db9f8b6.chunk.js.map",
"1604.a9d0b62b.chunk.js.map": "./static/js/1604.a9d0b62b.chunk.js.map",
"8391.7c39b52d.chunk.js.map": "./static/js/8391.7c39b52d.chunk.js.map",
"402.0fe11251.chunk.js.map": "./static/js/402.0fe11251.chunk.js.map",
"8391.a94a5da9.chunk.js.map": "./static/js/8391.a94a5da9.chunk.js.map",
"402.087a85de.chunk.js.map": "./static/js/402.087a85de.chunk.js.map",
"1705.5e57fd31.chunk.js.map": "./static/js/1705.5e57fd31.chunk.js.map",
"1581.e5ea40c0.chunk.js.map": "./static/js/1581.e5ea40c0.chunk.js.map",
"455.0218ce38.chunk.js.map": "./static/js/455.0218ce38.chunk.js.map",
"2661.2121f536.chunk.js.map": "./static/js/2661.2121f536.chunk.js.map",
"889.3d385602.chunk.js.map": "./static/js/889.3d385602.chunk.js.map",
"9088.e252c094.chunk.js.map": "./static/js/9088.e252c094.chunk.js.map",
"247.88de16aa.chunk.js.map": "./static/js/247.88de16aa.chunk.js.map",
"2763.44403c2a.chunk.js.map": "./static/js/2763.44403c2a.chunk.js.map",
"1581.cbc3b85a.chunk.js.map": "./static/js/1581.cbc3b85a.chunk.js.map",
"455.dd2c1a2c.chunk.js.map": "./static/js/455.dd2c1a2c.chunk.js.map",
"2661.ffb402c6.chunk.js.map": "./static/js/2661.ffb402c6.chunk.js.map",
"889.34a82de2.chunk.js.map": "./static/js/889.34a82de2.chunk.js.map",
"9088.edd377c6.chunk.js.map": "./static/js/9088.edd377c6.chunk.js.map",
"247.10687506.chunk.js.map": "./static/js/247.10687506.chunk.js.map",
"2763.247d377c.chunk.js.map": "./static/js/2763.247d377c.chunk.js.map",
"5171.2cf876b1.chunk.js.map": "./static/js/5171.2cf876b1.chunk.js.map",
"2426.172b5361.chunk.js.map": "./static/js/2426.172b5361.chunk.js.map",
"5561.c5000912.chunk.js.map": "./static/js/5561.c5000912.chunk.js.map",
@@ -292,12 +290,12 @@
"9785.7ccf0212.chunk.js.map": "./static/js/9785.7ccf0212.chunk.js.map",
"8735.52726eac.chunk.js.map": "./static/js/8735.52726eac.chunk.js.map",
"63.830fd6fc.chunk.js.map": "./static/js/63.830fd6fc.chunk.js.map",
"2983.66cf0ad4.chunk.js.map": "./static/js/2983.66cf0ad4.chunk.js.map",
"5289.8c388542.chunk.js.map": "./static/js/5289.8c388542.chunk.js.map",
"5026.0b30f6e2.chunk.js.map": "./static/js/5026.0b30f6e2.chunk.js.map"
"2983.3e4b1a45.chunk.js.map": "./static/js/2983.3e4b1a45.chunk.js.map",
"5289.1434b8bc.chunk.js.map": "./static/js/5289.1434b8bc.chunk.js.map",
"5026.ebd99276.chunk.js.map": "./static/js/5026.ebd99276.chunk.js.map"
},
"entrypoints": [
"static/css/main.90d417ae.css",
"static/js/main.eec275cb.js"
"static/js/main.7be4b5f3.js"
]
}

View File

@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><base href="/"/><meta content="width=device-width,initial-scale=1" name="viewport"/><meta content="#081C42" media="(prefers-color-scheme: light)" name="theme-color"/><meta content="#081C42" media="(prefers-color-scheme: dark)" name="theme-color"/><meta content="MinIO Console" name="description"/><link href="./styles/root-styles.css" rel="stylesheet"/><link href="./apple-icon-180x180.png" rel="apple-touch-icon" sizes="180x180"/><link href="./favicon-32x32.png" rel="icon" sizes="32x32" type="image/png"/><link href="./favicon-96x96.png" rel="icon" sizes="96x96" type="image/png"/><link href="./favicon-16x16.png" rel="icon" sizes="16x16" type="image/png"/><link href="./manifest.json" rel="manifest"/><link color="#3a4e54" href="./safari-pinned-tab.svg" rel="mask-icon"/><title>MinIO Console</title><script defer="defer" src="./static/js/main.eec275cb.js"></script><link href="./static/css/main.90d417ae.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"><div id="preload"><img src="./images/background.svg"/> <img src="./images/background-wave-orig2.svg"/></div><div id="loader-block"><img src="./Loader.svg"/></div></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><base href="/"/><meta content="width=device-width,initial-scale=1" name="viewport"/><meta content="#081C42" media="(prefers-color-scheme: light)" name="theme-color"/><meta content="#081C42" media="(prefers-color-scheme: dark)" name="theme-color"/><meta content="MinIO Console" name="description"/><link href="./styles/root-styles.css" rel="stylesheet"/><link href="./apple-icon-180x180.png" rel="apple-touch-icon" sizes="180x180"/><link href="./favicon-32x32.png" rel="icon" sizes="32x32" type="image/png"/><link href="./favicon-96x96.png" rel="icon" sizes="96x96" type="image/png"/><link href="./favicon-16x16.png" rel="icon" sizes="16x16" type="image/png"/><link href="./manifest.json" rel="manifest"/><link color="#3a4e54" href="./safari-pinned-tab.svg" rel="mask-icon"/><title>MinIO Console</title><script defer="defer" src="./static/js/main.7be4b5f3.js"></script><link href="./static/css/main.90d417ae.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"><div id="preload"><img src="./images/background.svg"/> <img src="./images/background-wave-orig2.svg"/></div><div id="loader-block"><img src="./Loader.svg"/></div></div></body></html>

View File

@@ -1,2 +1,2 @@
.cm-s-dracula .CodeMirror-gutters,.cm-s-dracula.CodeMirror{background-color:#282a36!important;border:none;color:#f8f8f2!important}.cm-s-dracula .CodeMirror-gutters{color:#282a36}.cm-s-dracula .CodeMirror-cursor{border-left:thin solid #f8f8f0}.cm-s-dracula .CodeMirror-linenumber{color:#6d8a88}.cm-s-dracula .CodeMirror-selected{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-line::selection,.cm-s-dracula .CodeMirror-line>span::selection,.cm-s-dracula .CodeMirror-line>span>span::selection{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-line::-moz-selection,.cm-s-dracula .CodeMirror-line>span::-moz-selection,.cm-s-dracula .CodeMirror-line>span>span::-moz-selection{background:hsla(0,0%,100%,.1)}.cm-s-dracula span.cm-comment{color:#6272a4}.cm-s-dracula span.cm-string,.cm-s-dracula span.cm-string-2{color:#f1fa8c}.cm-s-dracula span.cm-number{color:#bd93f9}.cm-s-dracula span.cm-variable{color:#50fa7b}.cm-s-dracula span.cm-variable-2{color:#fff}.cm-s-dracula span.cm-def{color:#50fa7b}.cm-s-dracula span.cm-keyword,.cm-s-dracula span.cm-operator{color:#ff79c6}.cm-s-dracula span.cm-atom{color:#bd93f9}.cm-s-dracula span.cm-meta{color:#f8f8f2}.cm-s-dracula span.cm-tag{color:#ff79c6}.cm-s-dracula span.cm-attribute,.cm-s-dracula span.cm-qualifier{color:#50fa7b}.cm-s-dracula span.cm-property{color:#66d9ef}.cm-s-dracula span.cm-builtin{color:#50fa7b}.cm-s-dracula span.cm-type,.cm-s-dracula span.cm-variable-3{color:#ffb86c}.cm-s-dracula .CodeMirror-activeline-background{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-matchingbracket{color:#fff!important;text-decoration:underline}
/*# sourceMappingURL=1955.8313f811.chunk.css.map*/
/*# sourceMappingURL=1955.04346438.chunk.css.map*/

View File

@@ -1 +1 @@
{"version":3,"file":"static/css/1955.8313f811.chunk.css","mappings":"AAUA,2DACE,kCAAoC,CAEpC,WAAY,CADZ,uBAEF,CACA,kCAAoC,aAAgB,CACpD,iCAAmC,8BAAiC,CACpE,qCAAuC,aAAgB,CACvD,mCAAqC,6BAAuC,CAC5E,6IAAuJ,6BAAuC,CAC9L,4JAAsK,6BAAuC,CAC7M,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAC/E,6BAA+B,aAAgB,CAC/C,+BAAiC,aAAgB,CACjD,iCAAmC,UAAc,CACjD,0BAA4B,aAAgB,CAE5C,6DAAgC,aAAgB,CAChD,2BAA6B,aAAgB,CAC7C,2BAA6B,aAAgB,CAC7C,0BAA4B,aAAgB,CAE5C,gEAAkC,aAAgB,CAClD,+BAAiC,aAAgB,CACjD,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAE/E,gDAAkD,6BAAmC,CACrF,0CAAwE,oBAAuB,CAAnD,yBAAqD","sources":["../node_modules/codemirror/theme/dracula.css"],"sourcesContent":["/*\n\n Name: dracula\n Author: Michael Kaminsky (http://github.com/mkaminsky11)\n\n Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme)\n\n*/\n\n\n.cm-s-dracula.CodeMirror, .cm-s-dracula .CodeMirror-gutters {\n background-color: #282a36 !important;\n color: #f8f8f2 !important;\n border: none;\n}\n.cm-s-dracula .CodeMirror-gutters { color: #282a36; }\n.cm-s-dracula .CodeMirror-cursor { border-left: solid thin #f8f8f0; }\n.cm-s-dracula .CodeMirror-linenumber { color: #6D8A88; }\n.cm-s-dracula .CodeMirror-selected { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::selection, .cm-s-dracula .CodeMirror-line > span::selection, .cm-s-dracula .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::-moz-selection, .cm-s-dracula .CodeMirror-line > span::-moz-selection, .cm-s-dracula .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula span.cm-comment { color: #6272a4; }\n.cm-s-dracula span.cm-string, .cm-s-dracula span.cm-string-2 { color: #f1fa8c; }\n.cm-s-dracula span.cm-number { color: #bd93f9; }\n.cm-s-dracula span.cm-variable { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-2 { color: white; }\n.cm-s-dracula span.cm-def { color: #50fa7b; }\n.cm-s-dracula span.cm-operator { color: #ff79c6; }\n.cm-s-dracula span.cm-keyword { color: #ff79c6; }\n.cm-s-dracula span.cm-atom { color: #bd93f9; }\n.cm-s-dracula span.cm-meta { color: #f8f8f2; }\n.cm-s-dracula span.cm-tag { color: #ff79c6; }\n.cm-s-dracula span.cm-attribute { color: #50fa7b; }\n.cm-s-dracula span.cm-qualifier { color: #50fa7b; }\n.cm-s-dracula span.cm-property { color: #66d9ef; }\n.cm-s-dracula span.cm-builtin { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-3, .cm-s-dracula span.cm-type { color: #ffb86c; }\n\n.cm-s-dracula .CodeMirror-activeline-background { background: rgba(255,255,255,0.1); }\n.cm-s-dracula .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }\n"],"names":[],"sourceRoot":""}
{"version":3,"file":"static/css/1955.04346438.chunk.css","mappings":"AAUA,2DACE,kCAAoC,CAEpC,WAAY,CADZ,uBAEF,CACA,kCAAoC,aAAgB,CACpD,iCAAmC,8BAAiC,CACpE,qCAAuC,aAAgB,CACvD,mCAAqC,6BAAuC,CAC5E,6IAAuJ,6BAAuC,CAC9L,4JAAsK,6BAAuC,CAC7M,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAC/E,6BAA+B,aAAgB,CAC/C,+BAAiC,aAAgB,CACjD,iCAAmC,UAAc,CACjD,0BAA4B,aAAgB,CAE5C,6DAAgC,aAAgB,CAChD,2BAA6B,aAAgB,CAC7C,2BAA6B,aAAgB,CAC7C,0BAA4B,aAAgB,CAE5C,gEAAkC,aAAgB,CAClD,+BAAiC,aAAgB,CACjD,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAE/E,gDAAkD,6BAAmC,CACrF,0CAAwE,oBAAuB,CAAnD,yBAAqD","sources":["../node_modules/codemirror/theme/dracula.css"],"sourcesContent":["/*\n\n Name: dracula\n Author: Michael Kaminsky (http://github.com/mkaminsky11)\n\n Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme)\n\n*/\n\n\n.cm-s-dracula.CodeMirror, .cm-s-dracula .CodeMirror-gutters {\n background-color: #282a36 !important;\n color: #f8f8f2 !important;\n border: none;\n}\n.cm-s-dracula .CodeMirror-gutters { color: #282a36; }\n.cm-s-dracula .CodeMirror-cursor { border-left: solid thin #f8f8f0; }\n.cm-s-dracula .CodeMirror-linenumber { color: #6D8A88; }\n.cm-s-dracula .CodeMirror-selected { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::selection, .cm-s-dracula .CodeMirror-line > span::selection, .cm-s-dracula .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::-moz-selection, .cm-s-dracula .CodeMirror-line > span::-moz-selection, .cm-s-dracula .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula span.cm-comment { color: #6272a4; }\n.cm-s-dracula span.cm-string, .cm-s-dracula span.cm-string-2 { color: #f1fa8c; }\n.cm-s-dracula span.cm-number { color: #bd93f9; }\n.cm-s-dracula span.cm-variable { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-2 { color: white; }\n.cm-s-dracula span.cm-def { color: #50fa7b; }\n.cm-s-dracula span.cm-operator { color: #ff79c6; }\n.cm-s-dracula span.cm-keyword { color: #ff79c6; }\n.cm-s-dracula span.cm-atom { color: #bd93f9; }\n.cm-s-dracula span.cm-meta { color: #f8f8f2; }\n.cm-s-dracula span.cm-tag { color: #ff79c6; }\n.cm-s-dracula span.cm-attribute { color: #50fa7b; }\n.cm-s-dracula span.cm-qualifier { color: #50fa7b; }\n.cm-s-dracula span.cm-property { color: #66d9ef; }\n.cm-s-dracula span.cm-builtin { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-3, .cm-s-dracula span.cm-type { color: #ffb86c; }\n\n.cm-s-dracula .CodeMirror-activeline-background { background: rgba(255,255,255,0.1); }\n.cm-s-dracula .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }\n"],"names":[],"sourceRoot":""}

View File

@@ -1,2 +1,2 @@
.cm-s-dracula .CodeMirror-gutters,.cm-s-dracula.CodeMirror{background-color:#282a36!important;border:none;color:#f8f8f2!important}.cm-s-dracula .CodeMirror-gutters{color:#282a36}.cm-s-dracula .CodeMirror-cursor{border-left:thin solid #f8f8f0}.cm-s-dracula .CodeMirror-linenumber{color:#6d8a88}.cm-s-dracula .CodeMirror-selected{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-line::selection,.cm-s-dracula .CodeMirror-line>span::selection,.cm-s-dracula .CodeMirror-line>span>span::selection{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-line::-moz-selection,.cm-s-dracula .CodeMirror-line>span::-moz-selection,.cm-s-dracula .CodeMirror-line>span>span::-moz-selection{background:hsla(0,0%,100%,.1)}.cm-s-dracula span.cm-comment{color:#6272a4}.cm-s-dracula span.cm-string,.cm-s-dracula span.cm-string-2{color:#f1fa8c}.cm-s-dracula span.cm-number{color:#bd93f9}.cm-s-dracula span.cm-variable{color:#50fa7b}.cm-s-dracula span.cm-variable-2{color:#fff}.cm-s-dracula span.cm-def{color:#50fa7b}.cm-s-dracula span.cm-keyword,.cm-s-dracula span.cm-operator{color:#ff79c6}.cm-s-dracula span.cm-atom{color:#bd93f9}.cm-s-dracula span.cm-meta{color:#f8f8f2}.cm-s-dracula span.cm-tag{color:#ff79c6}.cm-s-dracula span.cm-attribute,.cm-s-dracula span.cm-qualifier{color:#50fa7b}.cm-s-dracula span.cm-property{color:#66d9ef}.cm-s-dracula span.cm-builtin{color:#50fa7b}.cm-s-dracula span.cm-type,.cm-s-dracula span.cm-variable-3{color:#ffb86c}.cm-s-dracula .CodeMirror-activeline-background{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-matchingbracket{color:#fff!important;text-decoration:underline}
/*# sourceMappingURL=2080.8313f811.chunk.css.map*/
/*# sourceMappingURL=2080.04346438.chunk.css.map*/

View File

@@ -1 +1 @@
{"version":3,"file":"static/css/2080.8313f811.chunk.css","mappings":"AAUA,2DACE,kCAAoC,CAEpC,WAAY,CADZ,uBAEF,CACA,kCAAoC,aAAgB,CACpD,iCAAmC,8BAAiC,CACpE,qCAAuC,aAAgB,CACvD,mCAAqC,6BAAuC,CAC5E,6IAAuJ,6BAAuC,CAC9L,4JAAsK,6BAAuC,CAC7M,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAC/E,6BAA+B,aAAgB,CAC/C,+BAAiC,aAAgB,CACjD,iCAAmC,UAAc,CACjD,0BAA4B,aAAgB,CAE5C,6DAAgC,aAAgB,CAChD,2BAA6B,aAAgB,CAC7C,2BAA6B,aAAgB,CAC7C,0BAA4B,aAAgB,CAE5C,gEAAkC,aAAgB,CAClD,+BAAiC,aAAgB,CACjD,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAE/E,gDAAkD,6BAAmC,CACrF,0CAAwE,oBAAuB,CAAnD,yBAAqD","sources":["../node_modules/codemirror/theme/dracula.css"],"sourcesContent":["/*\n\n Name: dracula\n Author: Michael Kaminsky (http://github.com/mkaminsky11)\n\n Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme)\n\n*/\n\n\n.cm-s-dracula.CodeMirror, .cm-s-dracula .CodeMirror-gutters {\n background-color: #282a36 !important;\n color: #f8f8f2 !important;\n border: none;\n}\n.cm-s-dracula .CodeMirror-gutters { color: #282a36; }\n.cm-s-dracula .CodeMirror-cursor { border-left: solid thin #f8f8f0; }\n.cm-s-dracula .CodeMirror-linenumber { color: #6D8A88; }\n.cm-s-dracula .CodeMirror-selected { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::selection, .cm-s-dracula .CodeMirror-line > span::selection, .cm-s-dracula .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::-moz-selection, .cm-s-dracula .CodeMirror-line > span::-moz-selection, .cm-s-dracula .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula span.cm-comment { color: #6272a4; }\n.cm-s-dracula span.cm-string, .cm-s-dracula span.cm-string-2 { color: #f1fa8c; }\n.cm-s-dracula span.cm-number { color: #bd93f9; }\n.cm-s-dracula span.cm-variable { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-2 { color: white; }\n.cm-s-dracula span.cm-def { color: #50fa7b; }\n.cm-s-dracula span.cm-operator { color: #ff79c6; }\n.cm-s-dracula span.cm-keyword { color: #ff79c6; }\n.cm-s-dracula span.cm-atom { color: #bd93f9; }\n.cm-s-dracula span.cm-meta { color: #f8f8f2; }\n.cm-s-dracula span.cm-tag { color: #ff79c6; }\n.cm-s-dracula span.cm-attribute { color: #50fa7b; }\n.cm-s-dracula span.cm-qualifier { color: #50fa7b; }\n.cm-s-dracula span.cm-property { color: #66d9ef; }\n.cm-s-dracula span.cm-builtin { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-3, .cm-s-dracula span.cm-type { color: #ffb86c; }\n\n.cm-s-dracula .CodeMirror-activeline-background { background: rgba(255,255,255,0.1); }\n.cm-s-dracula .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }\n"],"names":[],"sourceRoot":""}
{"version":3,"file":"static/css/2080.04346438.chunk.css","mappings":"AAUA,2DACE,kCAAoC,CAEpC,WAAY,CADZ,uBAEF,CACA,kCAAoC,aAAgB,CACpD,iCAAmC,8BAAiC,CACpE,qCAAuC,aAAgB,CACvD,mCAAqC,6BAAuC,CAC5E,6IAAuJ,6BAAuC,CAC9L,4JAAsK,6BAAuC,CAC7M,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAC/E,6BAA+B,aAAgB,CAC/C,+BAAiC,aAAgB,CACjD,iCAAmC,UAAc,CACjD,0BAA4B,aAAgB,CAE5C,6DAAgC,aAAgB,CAChD,2BAA6B,aAAgB,CAC7C,2BAA6B,aAAgB,CAC7C,0BAA4B,aAAgB,CAE5C,gEAAkC,aAAgB,CAClD,+BAAiC,aAAgB,CACjD,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAE/E,gDAAkD,6BAAmC,CACrF,0CAAwE,oBAAuB,CAAnD,yBAAqD","sources":["../node_modules/codemirror/theme/dracula.css"],"sourcesContent":["/*\n\n Name: dracula\n Author: Michael Kaminsky (http://github.com/mkaminsky11)\n\n Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme)\n\n*/\n\n\n.cm-s-dracula.CodeMirror, .cm-s-dracula .CodeMirror-gutters {\n background-color: #282a36 !important;\n color: #f8f8f2 !important;\n border: none;\n}\n.cm-s-dracula .CodeMirror-gutters { color: #282a36; }\n.cm-s-dracula .CodeMirror-cursor { border-left: solid thin #f8f8f0; }\n.cm-s-dracula .CodeMirror-linenumber { color: #6D8A88; }\n.cm-s-dracula .CodeMirror-selected { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::selection, .cm-s-dracula .CodeMirror-line > span::selection, .cm-s-dracula .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::-moz-selection, .cm-s-dracula .CodeMirror-line > span::-moz-selection, .cm-s-dracula .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula span.cm-comment { color: #6272a4; }\n.cm-s-dracula span.cm-string, .cm-s-dracula span.cm-string-2 { color: #f1fa8c; }\n.cm-s-dracula span.cm-number { color: #bd93f9; }\n.cm-s-dracula span.cm-variable { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-2 { color: white; }\n.cm-s-dracula span.cm-def { color: #50fa7b; }\n.cm-s-dracula span.cm-operator { color: #ff79c6; }\n.cm-s-dracula span.cm-keyword { color: #ff79c6; }\n.cm-s-dracula span.cm-atom { color: #bd93f9; }\n.cm-s-dracula span.cm-meta { color: #f8f8f2; }\n.cm-s-dracula span.cm-tag { color: #ff79c6; }\n.cm-s-dracula span.cm-attribute { color: #50fa7b; }\n.cm-s-dracula span.cm-qualifier { color: #50fa7b; }\n.cm-s-dracula span.cm-property { color: #66d9ef; }\n.cm-s-dracula span.cm-builtin { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-3, .cm-s-dracula span.cm-type { color: #ffb86c; }\n\n.cm-s-dracula .CodeMirror-activeline-background { background: rgba(255,255,255,0.1); }\n.cm-s-dracula .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }\n"],"names":[],"sourceRoot":""}

View File

@@ -1,2 +1,2 @@
.cm-s-dracula .CodeMirror-gutters,.cm-s-dracula.CodeMirror{background-color:#282a36!important;border:none;color:#f8f8f2!important}.cm-s-dracula .CodeMirror-gutters{color:#282a36}.cm-s-dracula .CodeMirror-cursor{border-left:thin solid #f8f8f0}.cm-s-dracula .CodeMirror-linenumber{color:#6d8a88}.cm-s-dracula .CodeMirror-selected{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-line::selection,.cm-s-dracula .CodeMirror-line>span::selection,.cm-s-dracula .CodeMirror-line>span>span::selection{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-line::-moz-selection,.cm-s-dracula .CodeMirror-line>span::-moz-selection,.cm-s-dracula .CodeMirror-line>span>span::-moz-selection{background:hsla(0,0%,100%,.1)}.cm-s-dracula span.cm-comment{color:#6272a4}.cm-s-dracula span.cm-string,.cm-s-dracula span.cm-string-2{color:#f1fa8c}.cm-s-dracula span.cm-number{color:#bd93f9}.cm-s-dracula span.cm-variable{color:#50fa7b}.cm-s-dracula span.cm-variable-2{color:#fff}.cm-s-dracula span.cm-def{color:#50fa7b}.cm-s-dracula span.cm-keyword,.cm-s-dracula span.cm-operator{color:#ff79c6}.cm-s-dracula span.cm-atom{color:#bd93f9}.cm-s-dracula span.cm-meta{color:#f8f8f2}.cm-s-dracula span.cm-tag{color:#ff79c6}.cm-s-dracula span.cm-attribute,.cm-s-dracula span.cm-qualifier{color:#50fa7b}.cm-s-dracula span.cm-property{color:#66d9ef}.cm-s-dracula span.cm-builtin{color:#50fa7b}.cm-s-dracula span.cm-type,.cm-s-dracula span.cm-variable-3{color:#ffb86c}.cm-s-dracula .CodeMirror-activeline-background{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-matchingbracket{color:#fff!important;text-decoration:underline}
/*# sourceMappingURL=3368.8313f811.chunk.css.map*/
/*# sourceMappingURL=3631.04346438.chunk.css.map*/

View File

@@ -1 +1 @@
{"version":3,"file":"static/css/3368.8313f811.chunk.css","mappings":"AAUA,2DACE,kCAAoC,CAEpC,WAAY,CADZ,uBAEF,CACA,kCAAoC,aAAgB,CACpD,iCAAmC,8BAAiC,CACpE,qCAAuC,aAAgB,CACvD,mCAAqC,6BAAuC,CAC5E,6IAAuJ,6BAAuC,CAC9L,4JAAsK,6BAAuC,CAC7M,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAC/E,6BAA+B,aAAgB,CAC/C,+BAAiC,aAAgB,CACjD,iCAAmC,UAAc,CACjD,0BAA4B,aAAgB,CAE5C,6DAAgC,aAAgB,CAChD,2BAA6B,aAAgB,CAC7C,2BAA6B,aAAgB,CAC7C,0BAA4B,aAAgB,CAE5C,gEAAkC,aAAgB,CAClD,+BAAiC,aAAgB,CACjD,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAE/E,gDAAkD,6BAAmC,CACrF,0CAAwE,oBAAuB,CAAnD,yBAAqD","sources":["../node_modules/codemirror/theme/dracula.css"],"sourcesContent":["/*\n\n Name: dracula\n Author: Michael Kaminsky (http://github.com/mkaminsky11)\n\n Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme)\n\n*/\n\n\n.cm-s-dracula.CodeMirror, .cm-s-dracula .CodeMirror-gutters {\n background-color: #282a36 !important;\n color: #f8f8f2 !important;\n border: none;\n}\n.cm-s-dracula .CodeMirror-gutters { color: #282a36; }\n.cm-s-dracula .CodeMirror-cursor { border-left: solid thin #f8f8f0; }\n.cm-s-dracula .CodeMirror-linenumber { color: #6D8A88; }\n.cm-s-dracula .CodeMirror-selected { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::selection, .cm-s-dracula .CodeMirror-line > span::selection, .cm-s-dracula .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::-moz-selection, .cm-s-dracula .CodeMirror-line > span::-moz-selection, .cm-s-dracula .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula span.cm-comment { color: #6272a4; }\n.cm-s-dracula span.cm-string, .cm-s-dracula span.cm-string-2 { color: #f1fa8c; }\n.cm-s-dracula span.cm-number { color: #bd93f9; }\n.cm-s-dracula span.cm-variable { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-2 { color: white; }\n.cm-s-dracula span.cm-def { color: #50fa7b; }\n.cm-s-dracula span.cm-operator { color: #ff79c6; }\n.cm-s-dracula span.cm-keyword { color: #ff79c6; }\n.cm-s-dracula span.cm-atom { color: #bd93f9; }\n.cm-s-dracula span.cm-meta { color: #f8f8f2; }\n.cm-s-dracula span.cm-tag { color: #ff79c6; }\n.cm-s-dracula span.cm-attribute { color: #50fa7b; }\n.cm-s-dracula span.cm-qualifier { color: #50fa7b; }\n.cm-s-dracula span.cm-property { color: #66d9ef; }\n.cm-s-dracula span.cm-builtin { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-3, .cm-s-dracula span.cm-type { color: #ffb86c; }\n\n.cm-s-dracula .CodeMirror-activeline-background { background: rgba(255,255,255,0.1); }\n.cm-s-dracula .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }\n"],"names":[],"sourceRoot":""}
{"version":3,"file":"static/css/3631.04346438.chunk.css","mappings":"AAUA,2DACE,kCAAoC,CAEpC,WAAY,CADZ,uBAEF,CACA,kCAAoC,aAAgB,CACpD,iCAAmC,8BAAiC,CACpE,qCAAuC,aAAgB,CACvD,mCAAqC,6BAAuC,CAC5E,6IAAuJ,6BAAuC,CAC9L,4JAAsK,6BAAuC,CAC7M,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAC/E,6BAA+B,aAAgB,CAC/C,+BAAiC,aAAgB,CACjD,iCAAmC,UAAc,CACjD,0BAA4B,aAAgB,CAE5C,6DAAgC,aAAgB,CAChD,2BAA6B,aAAgB,CAC7C,2BAA6B,aAAgB,CAC7C,0BAA4B,aAAgB,CAE5C,gEAAkC,aAAgB,CAClD,+BAAiC,aAAgB,CACjD,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAE/E,gDAAkD,6BAAmC,CACrF,0CAAwE,oBAAuB,CAAnD,yBAAqD","sources":["../node_modules/codemirror/theme/dracula.css"],"sourcesContent":["/*\n\n Name: dracula\n Author: Michael Kaminsky (http://github.com/mkaminsky11)\n\n Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme)\n\n*/\n\n\n.cm-s-dracula.CodeMirror, .cm-s-dracula .CodeMirror-gutters {\n background-color: #282a36 !important;\n color: #f8f8f2 !important;\n border: none;\n}\n.cm-s-dracula .CodeMirror-gutters { color: #282a36; }\n.cm-s-dracula .CodeMirror-cursor { border-left: solid thin #f8f8f0; }\n.cm-s-dracula .CodeMirror-linenumber { color: #6D8A88; }\n.cm-s-dracula .CodeMirror-selected { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::selection, .cm-s-dracula .CodeMirror-line > span::selection, .cm-s-dracula .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::-moz-selection, .cm-s-dracula .CodeMirror-line > span::-moz-selection, .cm-s-dracula .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula span.cm-comment { color: #6272a4; }\n.cm-s-dracula span.cm-string, .cm-s-dracula span.cm-string-2 { color: #f1fa8c; }\n.cm-s-dracula span.cm-number { color: #bd93f9; }\n.cm-s-dracula span.cm-variable { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-2 { color: white; }\n.cm-s-dracula span.cm-def { color: #50fa7b; }\n.cm-s-dracula span.cm-operator { color: #ff79c6; }\n.cm-s-dracula span.cm-keyword { color: #ff79c6; }\n.cm-s-dracula span.cm-atom { color: #bd93f9; }\n.cm-s-dracula span.cm-meta { color: #f8f8f2; }\n.cm-s-dracula span.cm-tag { color: #ff79c6; }\n.cm-s-dracula span.cm-attribute { color: #50fa7b; }\n.cm-s-dracula span.cm-qualifier { color: #50fa7b; }\n.cm-s-dracula span.cm-property { color: #66d9ef; }\n.cm-s-dracula span.cm-builtin { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-3, .cm-s-dracula span.cm-type { color: #ffb86c; }\n\n.cm-s-dracula .CodeMirror-activeline-background { background: rgba(255,255,255,0.1); }\n.cm-s-dracula .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }\n"],"names":[],"sourceRoot":""}

View File

@@ -1,2 +0,0 @@
.cm-s-dracula .CodeMirror-gutters,.cm-s-dracula.CodeMirror{background-color:#282a36!important;border:none;color:#f8f8f2!important}.cm-s-dracula .CodeMirror-gutters{color:#282a36}.cm-s-dracula .CodeMirror-cursor{border-left:thin solid #f8f8f0}.cm-s-dracula .CodeMirror-linenumber{color:#6d8a88}.cm-s-dracula .CodeMirror-selected{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-line::selection,.cm-s-dracula .CodeMirror-line>span::selection,.cm-s-dracula .CodeMirror-line>span>span::selection{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-line::-moz-selection,.cm-s-dracula .CodeMirror-line>span::-moz-selection,.cm-s-dracula .CodeMirror-line>span>span::-moz-selection{background:hsla(0,0%,100%,.1)}.cm-s-dracula span.cm-comment{color:#6272a4}.cm-s-dracula span.cm-string,.cm-s-dracula span.cm-string-2{color:#f1fa8c}.cm-s-dracula span.cm-number{color:#bd93f9}.cm-s-dracula span.cm-variable{color:#50fa7b}.cm-s-dracula span.cm-variable-2{color:#fff}.cm-s-dracula span.cm-def{color:#50fa7b}.cm-s-dracula span.cm-keyword,.cm-s-dracula span.cm-operator{color:#ff79c6}.cm-s-dracula span.cm-atom{color:#bd93f9}.cm-s-dracula span.cm-meta{color:#f8f8f2}.cm-s-dracula span.cm-tag{color:#ff79c6}.cm-s-dracula span.cm-attribute,.cm-s-dracula span.cm-qualifier{color:#50fa7b}.cm-s-dracula span.cm-property{color:#66d9ef}.cm-s-dracula span.cm-builtin{color:#50fa7b}.cm-s-dracula span.cm-type,.cm-s-dracula span.cm-variable-3{color:#ffb86c}.cm-s-dracula .CodeMirror-activeline-background{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-matchingbracket{color:#fff!important;text-decoration:underline}
/*# sourceMappingURL=3688.8313f811.chunk.css.map*/

View File

@@ -1 +0,0 @@
{"version":3,"file":"static/css/3688.8313f811.chunk.css","mappings":"AAUA,2DACE,kCAAoC,CAEpC,WAAY,CADZ,uBAEF,CACA,kCAAoC,aAAgB,CACpD,iCAAmC,8BAAiC,CACpE,qCAAuC,aAAgB,CACvD,mCAAqC,6BAAuC,CAC5E,6IAAuJ,6BAAuC,CAC9L,4JAAsK,6BAAuC,CAC7M,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAC/E,6BAA+B,aAAgB,CAC/C,+BAAiC,aAAgB,CACjD,iCAAmC,UAAc,CACjD,0BAA4B,aAAgB,CAE5C,6DAAgC,aAAgB,CAChD,2BAA6B,aAAgB,CAC7C,2BAA6B,aAAgB,CAC7C,0BAA4B,aAAgB,CAE5C,gEAAkC,aAAgB,CAClD,+BAAiC,aAAgB,CACjD,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAE/E,gDAAkD,6BAAmC,CACrF,0CAAwE,oBAAuB,CAAnD,yBAAqD","sources":["../node_modules/codemirror/theme/dracula.css"],"sourcesContent":["/*\n\n Name: dracula\n Author: Michael Kaminsky (http://github.com/mkaminsky11)\n\n Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme)\n\n*/\n\n\n.cm-s-dracula.CodeMirror, .cm-s-dracula .CodeMirror-gutters {\n background-color: #282a36 !important;\n color: #f8f8f2 !important;\n border: none;\n}\n.cm-s-dracula .CodeMirror-gutters { color: #282a36; }\n.cm-s-dracula .CodeMirror-cursor { border-left: solid thin #f8f8f0; }\n.cm-s-dracula .CodeMirror-linenumber { color: #6D8A88; }\n.cm-s-dracula .CodeMirror-selected { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::selection, .cm-s-dracula .CodeMirror-line > span::selection, .cm-s-dracula .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::-moz-selection, .cm-s-dracula .CodeMirror-line > span::-moz-selection, .cm-s-dracula .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula span.cm-comment { color: #6272a4; }\n.cm-s-dracula span.cm-string, .cm-s-dracula span.cm-string-2 { color: #f1fa8c; }\n.cm-s-dracula span.cm-number { color: #bd93f9; }\n.cm-s-dracula span.cm-variable { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-2 { color: white; }\n.cm-s-dracula span.cm-def { color: #50fa7b; }\n.cm-s-dracula span.cm-operator { color: #ff79c6; }\n.cm-s-dracula span.cm-keyword { color: #ff79c6; }\n.cm-s-dracula span.cm-atom { color: #bd93f9; }\n.cm-s-dracula span.cm-meta { color: #f8f8f2; }\n.cm-s-dracula span.cm-tag { color: #ff79c6; }\n.cm-s-dracula span.cm-attribute { color: #50fa7b; }\n.cm-s-dracula span.cm-qualifier { color: #50fa7b; }\n.cm-s-dracula span.cm-property { color: #66d9ef; }\n.cm-s-dracula span.cm-builtin { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-3, .cm-s-dracula span.cm-type { color: #ffb86c; }\n\n.cm-s-dracula .CodeMirror-activeline-background { background: rgba(255,255,255,0.1); }\n.cm-s-dracula .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }\n"],"names":[],"sourceRoot":""}

View File

@@ -1,2 +1,2 @@
.cm-s-dracula .CodeMirror-gutters,.cm-s-dracula.CodeMirror{background-color:#282a36!important;border:none;color:#f8f8f2!important}.cm-s-dracula .CodeMirror-gutters{color:#282a36}.cm-s-dracula .CodeMirror-cursor{border-left:thin solid #f8f8f0}.cm-s-dracula .CodeMirror-linenumber{color:#6d8a88}.cm-s-dracula .CodeMirror-selected{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-line::selection,.cm-s-dracula .CodeMirror-line>span::selection,.cm-s-dracula .CodeMirror-line>span>span::selection{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-line::-moz-selection,.cm-s-dracula .CodeMirror-line>span::-moz-selection,.cm-s-dracula .CodeMirror-line>span>span::-moz-selection{background:hsla(0,0%,100%,.1)}.cm-s-dracula span.cm-comment{color:#6272a4}.cm-s-dracula span.cm-string,.cm-s-dracula span.cm-string-2{color:#f1fa8c}.cm-s-dracula span.cm-number{color:#bd93f9}.cm-s-dracula span.cm-variable{color:#50fa7b}.cm-s-dracula span.cm-variable-2{color:#fff}.cm-s-dracula span.cm-def{color:#50fa7b}.cm-s-dracula span.cm-keyword,.cm-s-dracula span.cm-operator{color:#ff79c6}.cm-s-dracula span.cm-atom{color:#bd93f9}.cm-s-dracula span.cm-meta{color:#f8f8f2}.cm-s-dracula span.cm-tag{color:#ff79c6}.cm-s-dracula span.cm-attribute,.cm-s-dracula span.cm-qualifier{color:#50fa7b}.cm-s-dracula span.cm-property{color:#66d9ef}.cm-s-dracula span.cm-builtin{color:#50fa7b}.cm-s-dracula span.cm-type,.cm-s-dracula span.cm-variable-3{color:#ffb86c}.cm-s-dracula .CodeMirror-activeline-background{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-matchingbracket{color:#fff!important;text-decoration:underline}
/*# sourceMappingURL=380.8313f811.chunk.css.map*/
/*# sourceMappingURL=380.04346438.chunk.css.map*/

View File

@@ -1 +1 @@
{"version":3,"file":"static/css/380.8313f811.chunk.css","mappings":"AAUA,2DACE,kCAAoC,CAEpC,WAAY,CADZ,uBAEF,CACA,kCAAoC,aAAgB,CACpD,iCAAmC,8BAAiC,CACpE,qCAAuC,aAAgB,CACvD,mCAAqC,6BAAuC,CAC5E,6IAAuJ,6BAAuC,CAC9L,4JAAsK,6BAAuC,CAC7M,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAC/E,6BAA+B,aAAgB,CAC/C,+BAAiC,aAAgB,CACjD,iCAAmC,UAAc,CACjD,0BAA4B,aAAgB,CAE5C,6DAAgC,aAAgB,CAChD,2BAA6B,aAAgB,CAC7C,2BAA6B,aAAgB,CAC7C,0BAA4B,aAAgB,CAE5C,gEAAkC,aAAgB,CAClD,+BAAiC,aAAgB,CACjD,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAE/E,gDAAkD,6BAAmC,CACrF,0CAAwE,oBAAuB,CAAnD,yBAAqD","sources":["../node_modules/codemirror/theme/dracula.css"],"sourcesContent":["/*\n\n Name: dracula\n Author: Michael Kaminsky (http://github.com/mkaminsky11)\n\n Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme)\n\n*/\n\n\n.cm-s-dracula.CodeMirror, .cm-s-dracula .CodeMirror-gutters {\n background-color: #282a36 !important;\n color: #f8f8f2 !important;\n border: none;\n}\n.cm-s-dracula .CodeMirror-gutters { color: #282a36; }\n.cm-s-dracula .CodeMirror-cursor { border-left: solid thin #f8f8f0; }\n.cm-s-dracula .CodeMirror-linenumber { color: #6D8A88; }\n.cm-s-dracula .CodeMirror-selected { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::selection, .cm-s-dracula .CodeMirror-line > span::selection, .cm-s-dracula .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::-moz-selection, .cm-s-dracula .CodeMirror-line > span::-moz-selection, .cm-s-dracula .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula span.cm-comment { color: #6272a4; }\n.cm-s-dracula span.cm-string, .cm-s-dracula span.cm-string-2 { color: #f1fa8c; }\n.cm-s-dracula span.cm-number { color: #bd93f9; }\n.cm-s-dracula span.cm-variable { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-2 { color: white; }\n.cm-s-dracula span.cm-def { color: #50fa7b; }\n.cm-s-dracula span.cm-operator { color: #ff79c6; }\n.cm-s-dracula span.cm-keyword { color: #ff79c6; }\n.cm-s-dracula span.cm-atom { color: #bd93f9; }\n.cm-s-dracula span.cm-meta { color: #f8f8f2; }\n.cm-s-dracula span.cm-tag { color: #ff79c6; }\n.cm-s-dracula span.cm-attribute { color: #50fa7b; }\n.cm-s-dracula span.cm-qualifier { color: #50fa7b; }\n.cm-s-dracula span.cm-property { color: #66d9ef; }\n.cm-s-dracula span.cm-builtin { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-3, .cm-s-dracula span.cm-type { color: #ffb86c; }\n\n.cm-s-dracula .CodeMirror-activeline-background { background: rgba(255,255,255,0.1); }\n.cm-s-dracula .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }\n"],"names":[],"sourceRoot":""}
{"version":3,"file":"static/css/380.04346438.chunk.css","mappings":"AAUA,2DACE,kCAAoC,CAEpC,WAAY,CADZ,uBAEF,CACA,kCAAoC,aAAgB,CACpD,iCAAmC,8BAAiC,CACpE,qCAAuC,aAAgB,CACvD,mCAAqC,6BAAuC,CAC5E,6IAAuJ,6BAAuC,CAC9L,4JAAsK,6BAAuC,CAC7M,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAC/E,6BAA+B,aAAgB,CAC/C,+BAAiC,aAAgB,CACjD,iCAAmC,UAAc,CACjD,0BAA4B,aAAgB,CAE5C,6DAAgC,aAAgB,CAChD,2BAA6B,aAAgB,CAC7C,2BAA6B,aAAgB,CAC7C,0BAA4B,aAAgB,CAE5C,gEAAkC,aAAgB,CAClD,+BAAiC,aAAgB,CACjD,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAE/E,gDAAkD,6BAAmC,CACrF,0CAAwE,oBAAuB,CAAnD,yBAAqD","sources":["../node_modules/codemirror/theme/dracula.css"],"sourcesContent":["/*\n\n Name: dracula\n Author: Michael Kaminsky (http://github.com/mkaminsky11)\n\n Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme)\n\n*/\n\n\n.cm-s-dracula.CodeMirror, .cm-s-dracula .CodeMirror-gutters {\n background-color: #282a36 !important;\n color: #f8f8f2 !important;\n border: none;\n}\n.cm-s-dracula .CodeMirror-gutters { color: #282a36; }\n.cm-s-dracula .CodeMirror-cursor { border-left: solid thin #f8f8f0; }\n.cm-s-dracula .CodeMirror-linenumber { color: #6D8A88; }\n.cm-s-dracula .CodeMirror-selected { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::selection, .cm-s-dracula .CodeMirror-line > span::selection, .cm-s-dracula .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::-moz-selection, .cm-s-dracula .CodeMirror-line > span::-moz-selection, .cm-s-dracula .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula span.cm-comment { color: #6272a4; }\n.cm-s-dracula span.cm-string, .cm-s-dracula span.cm-string-2 { color: #f1fa8c; }\n.cm-s-dracula span.cm-number { color: #bd93f9; }\n.cm-s-dracula span.cm-variable { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-2 { color: white; }\n.cm-s-dracula span.cm-def { color: #50fa7b; }\n.cm-s-dracula span.cm-operator { color: #ff79c6; }\n.cm-s-dracula span.cm-keyword { color: #ff79c6; }\n.cm-s-dracula span.cm-atom { color: #bd93f9; }\n.cm-s-dracula span.cm-meta { color: #f8f8f2; }\n.cm-s-dracula span.cm-tag { color: #ff79c6; }\n.cm-s-dracula span.cm-attribute { color: #50fa7b; }\n.cm-s-dracula span.cm-qualifier { color: #50fa7b; }\n.cm-s-dracula span.cm-property { color: #66d9ef; }\n.cm-s-dracula span.cm-builtin { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-3, .cm-s-dracula span.cm-type { color: #ffb86c; }\n\n.cm-s-dracula .CodeMirror-activeline-background { background: rgba(255,255,255,0.1); }\n.cm-s-dracula .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }\n"],"names":[],"sourceRoot":""}

View File

@@ -1,2 +1,2 @@
.cm-s-dracula .CodeMirror-gutters,.cm-s-dracula.CodeMirror{background-color:#282a36!important;border:none;color:#f8f8f2!important}.cm-s-dracula .CodeMirror-gutters{color:#282a36}.cm-s-dracula .CodeMirror-cursor{border-left:thin solid #f8f8f0}.cm-s-dracula .CodeMirror-linenumber{color:#6d8a88}.cm-s-dracula .CodeMirror-selected{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-line::selection,.cm-s-dracula .CodeMirror-line>span::selection,.cm-s-dracula .CodeMirror-line>span>span::selection{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-line::-moz-selection,.cm-s-dracula .CodeMirror-line>span::-moz-selection,.cm-s-dracula .CodeMirror-line>span>span::-moz-selection{background:hsla(0,0%,100%,.1)}.cm-s-dracula span.cm-comment{color:#6272a4}.cm-s-dracula span.cm-string,.cm-s-dracula span.cm-string-2{color:#f1fa8c}.cm-s-dracula span.cm-number{color:#bd93f9}.cm-s-dracula span.cm-variable{color:#50fa7b}.cm-s-dracula span.cm-variable-2{color:#fff}.cm-s-dracula span.cm-def{color:#50fa7b}.cm-s-dracula span.cm-keyword,.cm-s-dracula span.cm-operator{color:#ff79c6}.cm-s-dracula span.cm-atom{color:#bd93f9}.cm-s-dracula span.cm-meta{color:#f8f8f2}.cm-s-dracula span.cm-tag{color:#ff79c6}.cm-s-dracula span.cm-attribute,.cm-s-dracula span.cm-qualifier{color:#50fa7b}.cm-s-dracula span.cm-property{color:#66d9ef}.cm-s-dracula span.cm-builtin{color:#50fa7b}.cm-s-dracula span.cm-type,.cm-s-dracula span.cm-variable-3{color:#ffb86c}.cm-s-dracula .CodeMirror-activeline-background{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-matchingbracket{color:#fff!important;text-decoration:underline}
/*# sourceMappingURL=3631.8313f811.chunk.css.map*/
/*# sourceMappingURL=6633.04346438.chunk.css.map*/

View File

@@ -1 +1 @@
{"version":3,"file":"static/css/3631.8313f811.chunk.css","mappings":"AAUA,2DACE,kCAAoC,CAEpC,WAAY,CADZ,uBAEF,CACA,kCAAoC,aAAgB,CACpD,iCAAmC,8BAAiC,CACpE,qCAAuC,aAAgB,CACvD,mCAAqC,6BAAuC,CAC5E,6IAAuJ,6BAAuC,CAC9L,4JAAsK,6BAAuC,CAC7M,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAC/E,6BAA+B,aAAgB,CAC/C,+BAAiC,aAAgB,CACjD,iCAAmC,UAAc,CACjD,0BAA4B,aAAgB,CAE5C,6DAAgC,aAAgB,CAChD,2BAA6B,aAAgB,CAC7C,2BAA6B,aAAgB,CAC7C,0BAA4B,aAAgB,CAE5C,gEAAkC,aAAgB,CAClD,+BAAiC,aAAgB,CACjD,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAE/E,gDAAkD,6BAAmC,CACrF,0CAAwE,oBAAuB,CAAnD,yBAAqD","sources":["../node_modules/codemirror/theme/dracula.css"],"sourcesContent":["/*\n\n Name: dracula\n Author: Michael Kaminsky (http://github.com/mkaminsky11)\n\n Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme)\n\n*/\n\n\n.cm-s-dracula.CodeMirror, .cm-s-dracula .CodeMirror-gutters {\n background-color: #282a36 !important;\n color: #f8f8f2 !important;\n border: none;\n}\n.cm-s-dracula .CodeMirror-gutters { color: #282a36; }\n.cm-s-dracula .CodeMirror-cursor { border-left: solid thin #f8f8f0; }\n.cm-s-dracula .CodeMirror-linenumber { color: #6D8A88; }\n.cm-s-dracula .CodeMirror-selected { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::selection, .cm-s-dracula .CodeMirror-line > span::selection, .cm-s-dracula .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::-moz-selection, .cm-s-dracula .CodeMirror-line > span::-moz-selection, .cm-s-dracula .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula span.cm-comment { color: #6272a4; }\n.cm-s-dracula span.cm-string, .cm-s-dracula span.cm-string-2 { color: #f1fa8c; }\n.cm-s-dracula span.cm-number { color: #bd93f9; }\n.cm-s-dracula span.cm-variable { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-2 { color: white; }\n.cm-s-dracula span.cm-def { color: #50fa7b; }\n.cm-s-dracula span.cm-operator { color: #ff79c6; }\n.cm-s-dracula span.cm-keyword { color: #ff79c6; }\n.cm-s-dracula span.cm-atom { color: #bd93f9; }\n.cm-s-dracula span.cm-meta { color: #f8f8f2; }\n.cm-s-dracula span.cm-tag { color: #ff79c6; }\n.cm-s-dracula span.cm-attribute { color: #50fa7b; }\n.cm-s-dracula span.cm-qualifier { color: #50fa7b; }\n.cm-s-dracula span.cm-property { color: #66d9ef; }\n.cm-s-dracula span.cm-builtin { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-3, .cm-s-dracula span.cm-type { color: #ffb86c; }\n\n.cm-s-dracula .CodeMirror-activeline-background { background: rgba(255,255,255,0.1); }\n.cm-s-dracula .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }\n"],"names":[],"sourceRoot":""}
{"version":3,"file":"static/css/6633.04346438.chunk.css","mappings":"AAUA,2DACE,kCAAoC,CAEpC,WAAY,CADZ,uBAEF,CACA,kCAAoC,aAAgB,CACpD,iCAAmC,8BAAiC,CACpE,qCAAuC,aAAgB,CACvD,mCAAqC,6BAAuC,CAC5E,6IAAuJ,6BAAuC,CAC9L,4JAAsK,6BAAuC,CAC7M,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAC/E,6BAA+B,aAAgB,CAC/C,+BAAiC,aAAgB,CACjD,iCAAmC,UAAc,CACjD,0BAA4B,aAAgB,CAE5C,6DAAgC,aAAgB,CAChD,2BAA6B,aAAgB,CAC7C,2BAA6B,aAAgB,CAC7C,0BAA4B,aAAgB,CAE5C,gEAAkC,aAAgB,CAClD,+BAAiC,aAAgB,CACjD,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAE/E,gDAAkD,6BAAmC,CACrF,0CAAwE,oBAAuB,CAAnD,yBAAqD","sources":["../node_modules/codemirror/theme/dracula.css"],"sourcesContent":["/*\n\n Name: dracula\n Author: Michael Kaminsky (http://github.com/mkaminsky11)\n\n Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme)\n\n*/\n\n\n.cm-s-dracula.CodeMirror, .cm-s-dracula .CodeMirror-gutters {\n background-color: #282a36 !important;\n color: #f8f8f2 !important;\n border: none;\n}\n.cm-s-dracula .CodeMirror-gutters { color: #282a36; }\n.cm-s-dracula .CodeMirror-cursor { border-left: solid thin #f8f8f0; }\n.cm-s-dracula .CodeMirror-linenumber { color: #6D8A88; }\n.cm-s-dracula .CodeMirror-selected { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::selection, .cm-s-dracula .CodeMirror-line > span::selection, .cm-s-dracula .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::-moz-selection, .cm-s-dracula .CodeMirror-line > span::-moz-selection, .cm-s-dracula .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula span.cm-comment { color: #6272a4; }\n.cm-s-dracula span.cm-string, .cm-s-dracula span.cm-string-2 { color: #f1fa8c; }\n.cm-s-dracula span.cm-number { color: #bd93f9; }\n.cm-s-dracula span.cm-variable { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-2 { color: white; }\n.cm-s-dracula span.cm-def { color: #50fa7b; }\n.cm-s-dracula span.cm-operator { color: #ff79c6; }\n.cm-s-dracula span.cm-keyword { color: #ff79c6; }\n.cm-s-dracula span.cm-atom { color: #bd93f9; }\n.cm-s-dracula span.cm-meta { color: #f8f8f2; }\n.cm-s-dracula span.cm-tag { color: #ff79c6; }\n.cm-s-dracula span.cm-attribute { color: #50fa7b; }\n.cm-s-dracula span.cm-qualifier { color: #50fa7b; }\n.cm-s-dracula span.cm-property { color: #66d9ef; }\n.cm-s-dracula span.cm-builtin { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-3, .cm-s-dracula span.cm-type { color: #ffb86c; }\n\n.cm-s-dracula .CodeMirror-activeline-background { background: rgba(255,255,255,0.1); }\n.cm-s-dracula .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }\n"],"names":[],"sourceRoot":""}

View File

@@ -0,0 +1,2 @@
.cm-s-dracula .CodeMirror-gutters,.cm-s-dracula.CodeMirror{background-color:#282a36!important;border:none;color:#f8f8f2!important}.cm-s-dracula .CodeMirror-gutters{color:#282a36}.cm-s-dracula .CodeMirror-cursor{border-left:thin solid #f8f8f0}.cm-s-dracula .CodeMirror-linenumber{color:#6d8a88}.cm-s-dracula .CodeMirror-selected{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-line::selection,.cm-s-dracula .CodeMirror-line>span::selection,.cm-s-dracula .CodeMirror-line>span>span::selection{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-line::-moz-selection,.cm-s-dracula .CodeMirror-line>span::-moz-selection,.cm-s-dracula .CodeMirror-line>span>span::-moz-selection{background:hsla(0,0%,100%,.1)}.cm-s-dracula span.cm-comment{color:#6272a4}.cm-s-dracula span.cm-string,.cm-s-dracula span.cm-string-2{color:#f1fa8c}.cm-s-dracula span.cm-number{color:#bd93f9}.cm-s-dracula span.cm-variable{color:#50fa7b}.cm-s-dracula span.cm-variable-2{color:#fff}.cm-s-dracula span.cm-def{color:#50fa7b}.cm-s-dracula span.cm-keyword,.cm-s-dracula span.cm-operator{color:#ff79c6}.cm-s-dracula span.cm-atom{color:#bd93f9}.cm-s-dracula span.cm-meta{color:#f8f8f2}.cm-s-dracula span.cm-tag{color:#ff79c6}.cm-s-dracula span.cm-attribute,.cm-s-dracula span.cm-qualifier{color:#50fa7b}.cm-s-dracula span.cm-property{color:#66d9ef}.cm-s-dracula span.cm-builtin{color:#50fa7b}.cm-s-dracula span.cm-type,.cm-s-dracula span.cm-variable-3{color:#ffb86c}.cm-s-dracula .CodeMirror-activeline-background{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-matchingbracket{color:#fff!important;text-decoration:underline}
/*# sourceMappingURL=6859.04346438.chunk.css.map*/

View File

@@ -0,0 +1 @@
{"version":3,"file":"static/css/6859.04346438.chunk.css","mappings":"AAUA,2DACE,kCAAoC,CAEpC,WAAY,CADZ,uBAEF,CACA,kCAAoC,aAAgB,CACpD,iCAAmC,8BAAiC,CACpE,qCAAuC,aAAgB,CACvD,mCAAqC,6BAAuC,CAC5E,6IAAuJ,6BAAuC,CAC9L,4JAAsK,6BAAuC,CAC7M,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAC/E,6BAA+B,aAAgB,CAC/C,+BAAiC,aAAgB,CACjD,iCAAmC,UAAc,CACjD,0BAA4B,aAAgB,CAE5C,6DAAgC,aAAgB,CAChD,2BAA6B,aAAgB,CAC7C,2BAA6B,aAAgB,CAC7C,0BAA4B,aAAgB,CAE5C,gEAAkC,aAAgB,CAClD,+BAAiC,aAAgB,CACjD,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAE/E,gDAAkD,6BAAmC,CACrF,0CAAwE,oBAAuB,CAAnD,yBAAqD","sources":["../node_modules/codemirror/theme/dracula.css"],"sourcesContent":["/*\n\n Name: dracula\n Author: Michael Kaminsky (http://github.com/mkaminsky11)\n\n Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme)\n\n*/\n\n\n.cm-s-dracula.CodeMirror, .cm-s-dracula .CodeMirror-gutters {\n background-color: #282a36 !important;\n color: #f8f8f2 !important;\n border: none;\n}\n.cm-s-dracula .CodeMirror-gutters { color: #282a36; }\n.cm-s-dracula .CodeMirror-cursor { border-left: solid thin #f8f8f0; }\n.cm-s-dracula .CodeMirror-linenumber { color: #6D8A88; }\n.cm-s-dracula .CodeMirror-selected { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::selection, .cm-s-dracula .CodeMirror-line > span::selection, .cm-s-dracula .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::-moz-selection, .cm-s-dracula .CodeMirror-line > span::-moz-selection, .cm-s-dracula .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula span.cm-comment { color: #6272a4; }\n.cm-s-dracula span.cm-string, .cm-s-dracula span.cm-string-2 { color: #f1fa8c; }\n.cm-s-dracula span.cm-number { color: #bd93f9; }\n.cm-s-dracula span.cm-variable { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-2 { color: white; }\n.cm-s-dracula span.cm-def { color: #50fa7b; }\n.cm-s-dracula span.cm-operator { color: #ff79c6; }\n.cm-s-dracula span.cm-keyword { color: #ff79c6; }\n.cm-s-dracula span.cm-atom { color: #bd93f9; }\n.cm-s-dracula span.cm-meta { color: #f8f8f2; }\n.cm-s-dracula span.cm-tag { color: #ff79c6; }\n.cm-s-dracula span.cm-attribute { color: #50fa7b; }\n.cm-s-dracula span.cm-qualifier { color: #50fa7b; }\n.cm-s-dracula span.cm-property { color: #66d9ef; }\n.cm-s-dracula span.cm-builtin { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-3, .cm-s-dracula span.cm-type { color: #ffb86c; }\n\n.cm-s-dracula .CodeMirror-activeline-background { background: rgba(255,255,255,0.1); }\n.cm-s-dracula .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }\n"],"names":[],"sourceRoot":""}

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