Compare commits

..

10 Commits

Author SHA1 Message Date
Daniel Valdivia
cf8472b04c wss for websockets on tls and single page application behavior (#107) 2020-05-08 16:36:08 -07:00
Alex
317a7ebbd3 Fixed error messages in mcs (#105)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-05-08 12:31:09 -07:00
Alex
63f4150232 Added pagination to users page (#100)
Added pagination to users page for mcs, this resolves #96

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

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

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

24465
CREDITS Normal file

File diff suppressed because it is too large Load Diff

2
go.mod
View File

@@ -18,7 +18,7 @@ require (
github.com/json-iterator/go v1.1.9
github.com/minio/cli v1.22.0
github.com/minio/mc v0.0.0-20200415193718-68b638f2f96c
github.com/minio/minio v0.0.0-20200428222040-c3c3e9087bc1
github.com/minio/minio v0.0.0-20200501193630-d1c8e9f31ba0
github.com/minio/minio-go/v6 v6.0.55-0.20200424204115-7506d2996b22
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/satori/go.uuid v1.2.0

20
go.sum
View File

@@ -30,7 +30,6 @@ github.com/alecthomas/participle v0.2.1 h1:4AVLj1viSGa4LG5HDXKXrm5xRx19SB/rS/skP
github.com/alecthomas/participle v0.2.1/go.mod h1:SW6HZGeZgSIpcUWX3fXpfZhuaWHnmoD5KCVaqSaNTkk=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190307165228-86c17b95fcd5 h1:nWDRPCyCltiTsANwC/n3QZH7Vww33Npq9MKqlwRzI/c=
github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190307165228-86c17b95fcd5/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
@@ -42,7 +41,6 @@ github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:l
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 h1:zV3ejI06GQ59hwDQAvmK1qxOQGB3WuVTRoY0okPTAv0=
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
github.com/aws/aws-sdk-go v1.20.21/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f h1:ZNv7On9kyUzm7fvRZumSyy/IUiSC7AzL0I1jKKtwooA=
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc=
github.com/bcicen/jstream v0.0.0-20190220045926-16c1f8af81c2 h1:M+TYzBcNIRyzPRg66ndEqUMd7oWDmhvdQmaPC6EZNwM=
github.com/bcicen/jstream v0.0.0-20190220045926-16c1f8af81c2/go.mod h1:RDu/qcrnpEdJC/p8tx34+YBFqqX71lB7dOX9QE+ZC4M=
@@ -403,10 +401,9 @@ github.com/minio/lsync v1.0.1 h1:AVvILxA976xc27hstd1oR+X9PQG0sPSom1MNb1ImfUs=
github.com/minio/lsync v1.0.1/go.mod h1:tCFzfo0dlvdGl70IT4IAK/5Wtgb0/BrTmo/jE8pArKA=
github.com/minio/mc v0.0.0-20200415193718-68b638f2f96c h1:JLr0fYpCleodj9nGB5hfsJU2zPdnNQKqa2bYsIvPhVw=
github.com/minio/mc v0.0.0-20200415193718-68b638f2f96c/go.mod h1:l9PuOY62zT7AQJqopDjfo/T22AIBJSb2yXPVZf4RlhM=
github.com/minio/minio v0.0.0-20200415191640-bde0f444dbab h1:9hlqghJl3e3HorXa6ADWsz6ECq790t4iQs07VD9JctM=
github.com/minio/minio v0.0.0-20200415191640-bde0f444dbab/go.mod h1:v8oQPMMaTkjDwp5cOz1WCElA4Ik+X+0y4On+VMk0fis=
github.com/minio/minio v0.0.0-20200428222040-c3c3e9087bc1 h1:DQjH/653WCerOeZCp3BxAgkmRiQybHYiprbTFs+brgA=
github.com/minio/minio v0.0.0-20200428222040-c3c3e9087bc1/go.mod h1:HxnN5FYGIii8ZH6d+LH5UNOSSIonbJkYPqP6gWelVO0=
github.com/minio/minio v0.0.0-20200501193630-d1c8e9f31ba0 h1:QxIz36O01LbKqJiz6HKeKCOC2afgydspkpahQ807msY=
github.com/minio/minio v0.0.0-20200501193630-d1c8e9f31ba0/go.mod h1:Vhlqz7Se0EgpgFiVxpvzF4Zz/h2LMx+EPKH96Aera5U=
github.com/minio/minio-go/v6 v6.0.53 h1:8jzpwiOzZ5Iz7/goFWqNZRICbyWYShbb5rARjrnSCNI=
github.com/minio/minio-go/v6 v6.0.53/go.mod h1:DIvC/IApeHX8q1BAMVCXSXwpmrmM+I+iBvhvztQorfI=
github.com/minio/minio-go/v6 v6.0.55-0.20200424204115-7506d2996b22 h1:nZEve4vdUhwHBoV18zRvPDgjL6NYyDJE5QJvz3l9bRs=
@@ -555,6 +552,7 @@ github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94 h1:0ngsPmuP6XIjiFRN
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@@ -596,6 +594,7 @@ github.com/xdg/stringprep v1.0.0 h1:d9X0esnoa3dFsV0FG35rAT0RIhYFlPq7MiP+DW89La0=
github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
@@ -626,6 +625,7 @@ golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191117063200-497ca9f6d64f h1:kz4KIr+xcPUsI3VMoqWfPMvtnJ6MGfiVwsWSVzphMO4=
golang.org/x/crypto v0.0.0-20191117063200-497ca9f6d64f/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6 h1:Sy5bstxEqwwbYs6n0/pBuxKENqOeZUgD45Gp3Q3pqLg=
@@ -635,6 +635,8 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -665,6 +667,8 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -713,7 +717,13 @@ golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgw
golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190914235951-31e00f45c22e h1:nOOVVcLC+/3MeovP40q5lCiWmP1Z1DaN8yn8ngU63hw=
golang.org/x/tools v0.0.0-20190914235951-31e00f45c22e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200428211428-0c9eba77bc32 h1:Xvf3ZQTm5bjXPxhI7g+dwqsCqadK1rcNtwtszuatetk=
golang.org/x/tools v0.0.0-20200428211428-0c9eba77bc32/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.5.0 h1:lj9SyhMzyoa38fgFF0oO2T6pjs5IzkLPKfVtxpyCRMM=
google.golang.org/api v0.5.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=

View File

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

View File

@@ -0,0 +1,37 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"github.com/go-openapi/strfmt"
)
// ServiceAccounts service accounts
//
// swagger:model serviceAccounts
type ServiceAccounts []string
// Validate validates this service accounts
func (m ServiceAccounts) Validate(formats strfmt.Registry) error {
return nil
}

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -16,6 +16,7 @@
import storage from "local-storage-fallback";
import request from "superagent";
import get from "lodash/get";
export class API {
invoke(method: string, url: string, data?: object) {
@@ -36,10 +37,16 @@ export class API {
onError(err: any) {
if (err.status) {
const errMessage: string =
(err.response.body && err.response.body.error) ||
err.status.toString(10);
return Promise.reject(errMessage);
const errMessage = get(
err.response,
"body.message",
err.status.toString()
);
const throwMessage =
errMessage.charAt(0).toUpperCase() + errMessage.slice(1);
return Promise.reject(throwMessage);
} else {
return Promise.reject("Unknown error");
}

View File

@@ -13,13 +13,7 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, {
useState,
useEffect,
createRef,
ChangeEvent,
useCallback
} from "react";
import React, { useState, useEffect, createRef, ChangeEvent } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Grid from "@material-ui/core/Grid";
import get from "lodash/get";
@@ -69,15 +63,19 @@ const CSVMultiSelector = ({
const bottomList = createRef<HTMLDivElement>();
// Use effect to get the initial values from props
useCallback(() => {
if (currentElements.length === 1 && currentElements[0] === "") {
const elementsSplitted = elements.split(",");
if (elementsSplitted[elementsSplitted.length - 1].trim() !== "") {
elementsSplitted.push("");
}
setCurrentElements(elementsSplitted);
useEffect(() => {
if (
currentElements.length === 1 &&
currentElements[0] === "" &&
elements &&
elements !== ""
) {
const elementsSplit = elements.split(",");
elementsSplit.push("");
setCurrentElements(elementsSplit);
}
}, [elements, setCurrentElements, currentElements]);
}, [elements, currentElements]);
// Use effect to send new values to onChange
useEffect(() => {

View File

@@ -25,18 +25,41 @@ import CSVMultiSelector from "../Common/FormComponents/CSVMultiSelector/CSVMulti
interface IConfGenericProps {
onChange: (newValue: IElementValue[]) => void;
fields: KVField[];
defaultVals?: IElementValue[];
classes: any;
}
const styles = (theme: Theme) => createStyles({});
// Function to get defined values,
//we make this because the backed sometimes don't return all the keys when there is an initial configuration
export const valueDef = (
key: string,
type: string,
defaults: IElementValue[]
) => {
let defValue = type === "on|off" ? "false" : "";
if (defaults.length > 0) {
const storedConfig = defaults.find(element => element.key === key);
if (storedConfig) {
defValue = storedConfig.value;
}
}
return defValue;
};
const ConfTargetGeneric = ({
onChange,
fields,
defaultVals,
classes
}: IConfGenericProps) => {
const [valueHolder, setValueHolder] = useState<IElementValue[]>([]);
const fieldsElements = !fields ? [] : fields;
const defValList = !defaultVals ? [] : defaultVals;
// Effect to create all the values to hold
useEffect(() => {
@@ -44,13 +67,13 @@ const ConfTargetGeneric = ({
fields.forEach(field => {
const stateInsert: IElementValue = {
key: field.name,
value: field.type === "on|off" ? "false" : ""
value: valueDef(field.name, field.type, defValList)
};
values.push(stateInsert);
});
setValueHolder(values);
}, [fields]);
}, [fields, defaultVals]);
useEffect(() => {
onChange(valueHolder);

View File

@@ -66,7 +66,6 @@ const ConfigurationsList = ({ classes }: IListConfiguration) => {
configuration_id: "",
configuration_label: ""
});
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState("");
const [filter, setFilter] = useState("");
@@ -99,7 +98,6 @@ const ConfigurationsList = ({ classes }: IListConfiguration) => {
<EditConfiguration
open={editScreenOpen}
closeModalAndRefresh={() => {
setIsLoading(true);
setEditScreenOpen(false);
}}
selectedConfiguration={selectedConfiguration}
@@ -141,7 +139,7 @@ const ConfigurationsList = ({ classes }: IListConfiguration) => {
columns={[
{ label: "Configuration", elementKey: "configuration_id" }
]}
isLoading={isLoading}
isLoading={false}
records={filteredRecords}
entityName="Configurations"
idField="configuration_id"

View File

@@ -15,8 +15,9 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useCallback, useEffect, useState } from "react";
import get from "lodash/get";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Button } from "@material-ui/core";
import { Button, LinearProgress } from "@material-ui/core";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
@@ -64,9 +65,27 @@ const EditConfiguration = ({
//Local States
const [valuesObj, setValueObj] = useState<IElementValue[]>([]);
const [saving, setSaving] = useState<boolean>(false);
const [addError, setError] = useState<string>("");
const [loadingConfig, setLoadingConfig] = useState<boolean>(true);
const [errorConfig, setErrorConfig] = useState<string>("");
const [configValues, setConfigValues] = useState<IElementValue[]>([]);
//Effects
useEffect(() => {
const configId = get(selectedConfiguration, "configuration_id", false);
if (configId) {
api
.invoke("GET", `/api/v1/configs/${configId}`)
.then(res => {
const keyVals = get(res, "key_values", []);
setConfigValues(keyVals);
})
.catch(err => {
setLoadingConfig(false);
setErrorConfig(err);
});
}
setLoadingConfig(false);
}, [selectedConfiguration]);
useEffect(() => {
if (saving) {
@@ -81,14 +100,14 @@ const EditConfiguration = ({
)
.then(res => {
setSaving(false);
setError("");
setErrorConfig("");
serverNeedsRestart(true);
closeModalAndRefresh();
})
.catch(err => {
setSaving(false);
setError(err);
setErrorConfig(err);
});
}
}, [
@@ -119,14 +138,14 @@ const EditConfiguration = ({
title={selectedConfiguration.configuration_label}
>
<React.Fragment>
{addError !== "" && (
{errorConfig !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
{errorConfig}
</Typography>
</Grid>
)}
@@ -136,6 +155,7 @@ const EditConfiguration = ({
fieldsConfigurations[selectedConfiguration.configuration_id]
}
onChange={onValueChange}
defaultVals={configValues}
/>
<Grid item xs={3} className={classes.buttonContainer}>
<Button
@@ -148,6 +168,11 @@ const EditConfiguration = ({
Save
</Button>
</Grid>
{loadingConfig && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
<Grid item xs={9} />
</form>
</React.Fragment>

View File

@@ -53,13 +53,11 @@ export const configurationElements: IConfigurationElement[] = [
{ configuration_id: "kms_kes", configuration_label: "KMS KES Configuration" },
{
configuration_id: "logger_webhook",
configuration_label: "Logger Webhook Configuration",
url: "/webhook/logger"
configuration_label: "Logger Webhook Configuration"
},
{
configuration_id: "audit_webhook",
configuration_label: "Audit Webhook Configuration",
url: "/webhook/audit"
configuration_label: "Audit Webhook Configuration"
}
];
@@ -119,6 +117,20 @@ export const fieldsConfigurations: any = {
tooltip: "Minimum number of access before caching an object",
type: "number"
},
{
name: "watermark_low",
required: false,
label: "Watermark Low",
tooltip: "Watermark Low",
type: "number"
},
{
name: "watermark_high",
required: false,
label: "Watermark High",
tooltip: "Watermark High",
type: "number"
},
{
name: "comment",
required: false,
@@ -129,12 +141,6 @@ export const fieldsConfigurations: any = {
}
],
compression: [
{
name: "minio_compress",
required: true,
label: "MinIO Compress",
type: "on|off"
},
{
name: "extensions",
required: false,
@@ -199,6 +205,13 @@ export const fieldsConfigurations: any = {
}
],
identity_openid: [
{
name: "config_url",
required: false,
label: "Config URL",
tooltip: "Config URL for Client ID configuration",
type: "string"
},
{
name: "client_id",
required: false,
@@ -206,10 +219,17 @@ export const fieldsConfigurations: any = {
type: "string"
},
{
name: "config_url",
name: "claim_name",
required: false,
label: "Config URL",
tooltip: "Config URL for Client ID configuration",
label: "Claim Name",
tooltip: "Claim Name",
type: "string"
},
{
name: "claim_prefix",
required: false,
label: "Claim Prefix",
tooltip: "Claim Prefix",
type: "string"
}
],
@@ -294,20 +314,31 @@ export const fieldsConfigurations: any = {
],
policy_opa: [
{
name: "opa_url",
name: "url",
required: true,
label: "OPA URL",
type: "string"
},
{
name: "auth_token",
required: true,
label: "Auth Token",
type: "string"
},
{
name: "policy_opa",
required: true,
label: "Policy OPA",
type: "string"
}
],
kms_vault: [],
kms_kes: [],
logger_webhook: [
{
name: "name",
name: "endpoint",
required: true,
label: "Name",
tooltip: "Name of the webhook",
label: "Endpoint",
type: "string"
},
{
@@ -315,20 +346,13 @@ export const fieldsConfigurations: any = {
required: true,
label: "Auth Token",
type: "string"
},
{
name: "endpoint",
required: true,
label: "Endpoint",
type: "string"
}
],
audit_webhook: [
{
name: "name",
name: "endpoint",
required: true,
label: "Name",
tooltip: "Name of the webhook",
label: "Endpoint",
type: "string"
},
{
@@ -336,12 +360,6 @@ export const fieldsConfigurations: any = {
required: true,
label: "Auth Token",
type: "string"
},
{
name: "endpoint",
required: true,
label: "Endpoint",
type: "string"
}
]
};

View File

@@ -62,6 +62,7 @@ import ConfigurationsList from "./Configurations/ConfigurationPanels/Configurati
import { Button, LinearProgress } from "@material-ui/core";
import WebhookPanel from "./Configurations/ConfigurationPanels/WebhookPanel";
import Trace from "./Trace/Trace";
import Logs from "./Logs/Logs";
function Copyright() {
return (
@@ -304,6 +305,7 @@ class Console extends React.Component<
<Route exact path="/webhook/logger" component={WebhookPanel} />
<Route exact path="/webhook/audit" component={WebhookPanel} />
<Route exct path="/trace" component={Trace} />
<Route exct path="/logs" component={Logs} />
<Route exact path="/">
<Redirect to="/dashboard" />
</Route>

View File

@@ -0,0 +1,314 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useEffect } from "react";
import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket";
import storage from "local-storage-fallback";
import { AppState } from "../../../store";
import { connect } from "react-redux";
import { logMessageReceived, logResetMessages } from "./actions";
import { LogMessage } from "./types";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { niceBytes } from "../../../common/utils";
import Ansi from "ansi-to-react";
import { isNull, isNullOrUndefined } from "util";
import { wsProtocol } from "../../../utils/wsUtils";
const styles = (theme: Theme) =>
createStyles({
logList: {
background: "white",
maxHeight: "400px",
overflow: "auto",
"& ul": {
margin: "4px",
padding: "0px"
},
"& ul li": {
listStyle: "none",
margin: "0px",
padding: "0px",
borderBottom: "1px solid #dedede"
}
},
tab: {
padding: "25px"
},
ansiblue: {
color: "blue"
},
logerror: {
color: "red"
},
logerror_tab: {
color: "red",
padding: "25px"
},
ansidefault: {
color: "black"
}
});
interface ILogs {
classes: any;
logMessageReceived: typeof logMessageReceived;
logResetMessages: typeof logResetMessages;
messages: LogMessage[];
}
const Logs = ({
classes,
logMessageReceived,
logResetMessages,
messages
}: ILogs) => {
useEffect(() => {
logResetMessages();
const url = new URL(window.location.toString());
const isDev = process.env.NODE_ENV === "development";
const port = isDev ? "9090" : url.port;
const wsProt = wsProtocol(url.protocol);
const c = new W3CWebSocket(
`${wsProt}://${url.hostname}:${port}/ws/console`
);
let interval: any | null = null;
if (c !== null) {
c.onopen = () => {
console.log("WebSocket Client Connected");
c.send("ok");
interval = setInterval(() => {
c.send("ok");
}, 10 * 1000);
};
c.onmessage = (message: IMessageEvent) => {
// console.log(message.data.toString())
let m: LogMessage = JSON.parse(message.data.toString());
m.time = new Date(m.time.toString());
m.key = Math.random();
logMessageReceived(m);
};
c.onclose = () => {
clearInterval(interval);
console.log("connection closed by server");
};
return () => {
c.close(1000);
clearInterval(interval);
console.log("closing websockets");
};
}
}, [logMessageReceived]);
const timeFromdate = (d: Date) => {
let h = d.getHours() < 10 ? `0${d.getHours()}` : `${d.getHours()}`;
let m = d.getMinutes() < 10 ? `0${d.getMinutes()}` : `${d.getMinutes()}`;
let s = d.getSeconds() < 10 ? `0${d.getSeconds()}` : `${d.getSeconds()}`;
return `${h}:${m}:${s}:${d.getMilliseconds()}`;
};
// replaces a character of a string with other at a given index
const replaceWeirdChar = (
origString: string,
replaceChar: string,
index: number
) => {
let firstPart = origString.substr(0, index);
let lastPart = origString.substr(index + 1);
let newString = firstPart + replaceChar + lastPart;
return newString;
};
const colorify = (str: string) => {
// matches strings starting like: `[34mEndpoint: [0m`
const colorRegex = /(\[[0-9]+m)(.*?)(\[0+m)/g;
let matches = colorRegex.exec(str);
if (!isNullOrUndefined(matches)) {
let start_color = matches[1];
let text = matches[2];
if (start_color === "[34m") {
return <span className={classes.ansiblue}>{text}</span>;
}
if (start_color === "[1m") {
return <span className={classes.ansidarkblue}>{text}</span>;
}
}
};
const renderError = (logElement: LogMessage) => {
let errorElems = [];
if (!isNullOrUndefined(logElement.error)) {
if (logElement.api && logElement.api.name) {
errorElems.push(
<li key={`api-${logElement.key}`}>
<span className={classes.logerror}>API: {logElement.api.name}</span>
</li>
);
}
if (logElement.time) {
errorElems.push(
<li key={`time-${logElement.key}`}>
<span className={classes.logerror}>
Time: {timeFromdate(logElement.time)}
</span>
</li>
);
}
if (logElement.deploymentid) {
errorElems.push(
<li key={`deploytmentid-${logElement.key}`}>
<span className={classes.logerror}>
DeploymentID: {logElement.deploymentid}
</span>
</li>
);
}
if (logElement.requestID) {
errorElems.push(
<li key={`requestid-${logElement.key}`}>
<span className={classes.logerror}>
RequestID: {logElement.requestID}
</span>
</li>
);
}
if (logElement.remotehost) {
errorElems.push(
<li key={`remotehost-${logElement.key}`}>
<span className={classes.logerror}>
RemoteHost: {logElement.remotehost}
</span>
</li>
);
}
if (logElement.host) {
errorElems.push(
<li key={`host-${logElement.key}`}>
<span className={classes.logerror}>Host: {logElement.host}</span>
</li>
);
}
if (logElement.userAgent) {
errorElems.push(
<li key={`useragent-${logElement.key}`}>
<span className={classes.logerror}>
UserAgent: {logElement.userAgent}
</span>
</li>
);
}
if (logElement.error.message) {
errorElems.push(
<li key={`message-${logElement.key}`}>
<span className={classes.logerror}>
Error: {logElement.error.message}
</span>
</li>
);
}
if (logElement.error.source) {
// for all sources add padding
for (let s in logElement.error.source) {
errorElems.push(
<li key={`source-${logElement.key}-${s}`}>
<span className={classes.logerror_tab}>
{logElement.error.source[s]}
</span>
</li>
);
}
}
}
return errorElems;
};
const renderLog = (logElement: LogMessage) => {
let logMessage = logElement.ConsoleMsg;
// if somehow after the color code starts with unicode 10 = Line feed
// delete it
const regexInit = /(\[[0-9]+m)/g;
let match = regexInit.exec(logMessage);
if (match) {
if (logMessage.slice(match[0].length).codePointAt(1) == 10) {
logMessage = replaceWeirdChar(logMessage, "", match[0].length + 1);
}
}
// Select what to add color and what not to.
const colorRegex = /(\[[0-9]+m)(.*?)(\[0+m)/g;
let m = colorRegex.exec(logMessage);
// get substring if there was a match for to split what
// is going to be colored and what not, here we add color
// only to the first match.
let substr = logMessage.slice(colorRegex.lastIndex);
substr = substr.replace(regexInit, "");
// strClean used for corner case when string has unicode 32 for
// space instead of normal space.
let strClean = logMessage.replace(regexInit, "");
// if starts with multiple spaces add padding
if (strClean.startsWith(" ") || strClean.codePointAt(1) === 32) {
return (
<li key={logElement.key}>
<span className={classes.tab}>
{colorify(logMessage)} {substr}
</span>
</li>
);
} else if (!isNullOrUndefined(logElement.error)) {
// list error message and all sources and error elems
return renderError(logElement);
} else {
// for all remaining set default class
return (
<li key={logElement.key}>
<span className={classes.ansidefault}>
{colorify(logMessage)} {substr}
</span>
</li>
);
}
};
return (
<div>
<h1>Logs</h1>
<div className={classes.logList}>
<ul>
{messages.map(m => {
return renderLog(m);
})}
</ul>
</div>
</div>
);
};
const mapState = (state: AppState) => ({
messages: state.logs.messages
});
const connector = connect(mapState, {
logMessageReceived: logMessageReceived,
logResetMessages: logResetMessages
});
export default connector(withStyles(styles)(Logs));

View File

@@ -0,0 +1,44 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { LogMessage } from "./types";
export const LOG_MESSAGE_RECEIVED = "LOG_MESSAGE_RECEIVED";
export const LOG_RESET_MESSAGES = "LOG_RESET_MESSAGES";
interface LogMessageReceivedAction {
type: typeof LOG_MESSAGE_RECEIVED;
message: LogMessage;
}
interface LogResetMessagesAction {
type: typeof LOG_RESET_MESSAGES;
}
export type LogActionTypes = LogMessageReceivedAction | LogResetMessagesAction;
export function logMessageReceived(message: LogMessage) {
return {
type: LOG_MESSAGE_RECEIVED,
message: message
};
}
export function logResetMessages() {
return {
type: LOG_RESET_MESSAGES
};
}

View File

@@ -0,0 +1,50 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import {
LOG_MESSAGE_RECEIVED,
LOG_RESET_MESSAGES,
LogActionTypes
} from "./actions";
import { LogMessage } from "./types";
export interface LogState {
messages: LogMessage[];
}
const initialState: LogState = {
messages: []
};
export function logReducer(
state = initialState,
action: LogActionTypes
): LogState {
switch (action.type) {
case LOG_MESSAGE_RECEIVED:
return {
...state,
messages: [...state.messages, action.message]
};
case LOG_RESET_MESSAGES:
return {
...state,
messages: []
};
default:
return state;
}
}

View File

@@ -0,0 +1,44 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
export interface logError {
message: string;
source: string[];
}
export interface logErrorApiArgs {
bucket: string;
object: string;
}
export interface logErrorApi {
name: string;
args: logErrorApiArgs;
}
export interface LogMessage {
remotehost: string;
host: string;
requestID: string;
userAgent: string;
message: string;
api: logErrorApi;
deploymentid: string;
time: Date;
error: logError;
ConsoleMsg: string;
key: number;
}

View File

@@ -17,6 +17,7 @@
import React from "react";
import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import WebAssetIcon from "@material-ui/icons/WebAsset";
import ListItemText from "@material-ui/core/ListItemText";
import { NavLink } from "react-router-dom";
import { Divider, Typography, withStyles } from "@material-ui/core";
@@ -157,6 +158,12 @@ class Menu extends React.Component<MenuProps> {
</ListItemIcon>
<ListItemText primary="Trace" />
</ListItem>
<ListItem button component={NavLink} to="/logs">
<ListItemIcon>
<WebAssetIcon />
</ListItemIcon>
<ListItemText primary="Console Logs" />
</ListItem>
<ListItem component={Typography}>Configuration</ListItem>
<ListItem button component={NavLink} to="/notification-endpoints">
<ListItemIcon>

View File

@@ -21,7 +21,8 @@ import { connect } from "react-redux";
import { traceMessageReceived, traceResetMessages } from "./actions";
import { TraceMessage } from "./types";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { niceBytes, setCookie } from "../../../common/utils";
import { niceBytes } from "../../../common/utils";
import { wsProtocol } from "../../../utils/wsUtils";
const styles = (theme: Theme) =>
createStyles({
@@ -57,14 +58,12 @@ const Trace = ({
}: ITrace) => {
useEffect(() => {
traceResetMessages();
const token: string = storage.getItem("token")!;
const url = new URL(window.location.toString());
const isDev = process.env.NODE_ENV === "development";
const port = isDev ? "9090" : url.port;
setCookie("token", token);
const c = new W3CWebSocket(`ws://${url.hostname}:${port}/ws/trace`);
const wsProt = wsProtocol(url.protocol);
const c = new W3CWebSocket(`${wsProt}://${url.hostname}:${port}/ws/trace`);
let interval: any | null = null;
if (c !== null) {

View File

@@ -195,24 +195,25 @@ class Users extends React.Component<IUsersProps, IUsersState> {
} = this.state;
const handleChangePage = (event: unknown, newPage: number) => {
this.setState({ page: newPage }, () => {
this.fetchRecords();
});
this.setState({ page: newPage });
};
const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement>
) => {
const rPP = parseInt(event.target.value, 10);
this.setState({ page: 0, rowsPerPage: rPP }, () => {
this.fetchRecords();
});
this.setState({ page: 0, rowsPerPage: rPP });
};
const filteredRecords = records.filter(elementItem =>
elementItem.accessKey.includes(filter)
);
const beginRecord = page * rowsPerPage;
const endRecords = beginRecord + rowsPerPage;
const paginatedRecords = filteredRecords.slice(beginRecord, endRecords);
const selectionChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
const targetD = e.target;
const value = targetD.value;
@@ -306,7 +307,7 @@ class Users extends React.Component<IUsersProps, IUsersState> {
)
}}
onChange={e => {
this.setState({ filter: e.target.value });
this.setState({ filter: e.target.value, page: 0 });
}}
/>
<Button
@@ -349,13 +350,13 @@ class Users extends React.Component<IUsersProps, IUsersState> {
onSelect={selectionChanged}
selectedItems={checkedUsers}
isLoading={loading}
records={filteredRecords}
records={paginatedRecords}
entityName="Users"
idField="accessKey"
paginatorConfig={{
rowsPerPageOptions: [5, 10, 25],
colSpan: 3,
count: totalRecords,
count: filteredRecords.length,
rowsPerPage: rowsPerPage,
page: page,
SelectProps: {

View File

@@ -14,17 +14,17 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, {FC, useEffect} from "react";
import {RouteComponentProps} from "react-router";
import React, { FC, useEffect } from "react";
import { RouteComponentProps } from "react-router";
import storage from "local-storage-fallback";
import api from "../../common/api";
const LoginCallback: FC<RouteComponentProps> = ({location}) => {
const LoginCallback: FC<RouteComponentProps> = ({ location }) => {
useEffect(() => {
const code = (location.search.match(/code=([^&]+)/) || [])[1];
const state = (location.search.match(/state=([^&]+)/) || [])[1];
api
.invoke("POST", "/api/v1/login/oauth2/auth", {code, state})
.invoke("POST", "/api/v1/login/oauth2/auth", { code, state })
.then((res: any) => {
if (res && res.sessionId) {
// store the jwt token

View File

@@ -29,13 +29,14 @@ import { userLoggedIn } from "../../actions";
import history from "../../history";
import api from "../../common/api";
import { ILoginDetails } from "./types";
import { setCookie } from "../../common/utils";
const styles = (theme: Theme) =>
createStyles({
"@global": {
body: {
backgroundColor: "#F4F4F4",
},
backgroundColor: "#F4F4F4"
}
},
paper: {
marginTop: theme.spacing(16),
@@ -44,48 +45,48 @@ const styles = (theme: Theme) =>
flexDirection: "column",
alignItems: "center",
width: "800px",
margin: "auto",
margin: "auto"
},
avatar: {
margin: theme.spacing(1),
backgroundColor: theme.palette.secondary.main,
backgroundColor: theme.palette.secondary.main
},
form: {
width: "100%", // Fix IE 11 issue.
marginTop: theme.spacing(3),
marginTop: theme.spacing(3)
},
submit: {
margin: theme.spacing(3, 0, 2),
margin: theme.spacing(3, 0, 2)
},
errorBlock: {
color: "red",
color: "red"
},
mainContainer: {
borderRadius: "3px",
borderRadius: "3px"
},
theOcean: {
borderTopLeftRadius: "3px",
borderBottomLeftRadius: "3px",
background:
"transparent linear-gradient(333deg, #281B6F 1%, #271260 13%, #120D53 83%) 0% 0% no-repeat padding-box;",
"transparent linear-gradient(333deg, #281B6F 1%, #271260 13%, #120D53 83%) 0% 0% no-repeat padding-box;"
},
oceanBg: {
backgroundImage: "url(/images/BG_Illustration.svg)",
backgroundRepeat: "no-repeat",
backgroundPosition: "bottom left",
height: "100%",
width: "100%",
width: "100%"
},
theLogin: {
padding: "76px 62px 20px 62px",
padding: "76px 62px 20px 62px"
},
loadingLoginStrategy: {
textAlign: "center",
},
textAlign: "center"
}
});
const mapState = (state: SystemState) => ({
loggedIn: state.loggedIn,
loggedIn: state.loggedIn
});
const connector = connect(mapState, { userLoggedIn });
@@ -116,8 +117,8 @@ class Login extends React.Component<ILoginProps, ILoginState> {
loading: false,
loginStrategy: {
loginStrategy: "",
redirect: "",
},
redirect: ""
}
};
fetchConfiguration() {
@@ -126,12 +127,12 @@ class Login extends React.Component<ILoginProps, ILoginState> {
.invoke("GET", "/api/v1/login")
.then((loginDetails: ILoginDetails) => {
this.setState({
loading: false,
loading: false
});
this.setState({
loading: false,
loginStrategy: loginDetails,
error: "",
error: ""
});
})
.catch((err: any) => {
@@ -153,6 +154,7 @@ class Login extends React.Component<ILoginProps, ILoginState> {
if (bodyResponse.sessionId) {
// store the jwt token
setCookie("token", bodyResponse.sessionId);
storage.setItem("token", bodyResponse.sessionId);
//return res.body.sessionId;
} else if (bodyResponse.error) {
@@ -166,7 +168,7 @@ class Login extends React.Component<ILoginProps, ILoginState> {
// We push to history the new URL.
history.push("/dashboard");
})
.catch((err) => {
.catch(err => {
this.setState({ error: `${err}` });
});
};

View File

@@ -18,10 +18,12 @@ import { applyMiddleware, combineReducers, compose, createStore } from "redux";
import thunk from "redux-thunk";
import { systemReducer } from "./reducer";
import { traceReducer } from "./screens/Console/Trace/reducers";
import { logReducer } from "./screens/Console/Logs/reducers";
const globalReducer = combineReducers({
system: systemReducer,
trace: traceReducer
trace: traceReducer,
logs: logReducer
});
declare global {

View File

@@ -0,0 +1,22 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
export const wsProtocol = (protocol: string): string => {
let wsProtocol = "ws";
if (protocol == "https:") {
wsProtocol = "wss";
}
return wsProtocol;
};

View File

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

View File

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

147
restapi/admin_console.go Normal file
View File

@@ -0,0 +1,147 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package restapi
import (
"context"
"encoding/json"
"fmt"
"log"
"strings"
"sync"
"time"
"github.com/gorilla/websocket"
"github.com/minio/minio/pkg/madmin"
)
const logTimeFormat string = "15:04:05 MST 01/02/2006"
// startConsoleLog starts log of the servers
// by first setting a websocket reader that will
// check for a heartbeat.
//
// A WaitGroup is used to handle goroutines and to ensure
// all finish in the proper order. If any, sendConsoleLogInfo()
// or wsReadCheck() returns, trace should end.
func startConsoleLog(conn WSConn, client MinioAdmin) (mError error) {
// a WaitGroup waits for a collection of goroutines to finish
wg := sync.WaitGroup{}
// a cancel context is needed to end all goroutines used
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Set number of goroutines to wait. wg.Wait()
// waitsuntil counter is zero (all are done)
wg.Add(3)
// start go routine for reading websocket heartbeat
readErr := wsReadCheck(ctx, &wg, conn)
// send Stream of Console Log Info to the ws c.connection
logCh := sendConsoleLogInfo(ctx, &wg, conn, client)
// If wsReadCheck returns it means that it is not possible to check
// ws heartbeat anymore so we stop from doing Console Log, cancel context
// for all goroutines.
go func(wg *sync.WaitGroup) {
defer wg.Done()
if err := <-readErr; err != nil {
log.Println("error on wsReadCheck:", err)
mError = err
}
// cancel context for all goroutines.
cancel()
}(&wg)
// get logCh err on finish
if err := <-logCh; err != nil {
mError = err
}
// if logCh closes for any reason,
// cancel context for all goroutines
cancel()
// wait all goroutines to finish
wg.Wait()
return mError
}
// sendlogInfo sends stream of Console Log Info to the ws connection
func sendConsoleLogInfo(ctx context.Context, wg *sync.WaitGroup, conn WSConn, client MinioAdmin) <-chan error {
// decrements the WaitGroup counter
// by one when the function returns
defer wg.Done()
ch := make(chan error)
go func(ch chan<- error) {
defer close(ch)
// TODO: accept parameters as variables
// name of node, default = "" (all)
node := ""
// number of log lines
lineCount := 100
// type of logs "minio"|"application"|"all" default = "all"
logKind := "all"
// Start listening on all Console Log activity.
logCh := client.getLogs(ctx, node, lineCount, logKind)
for logInfo := range logCh {
if logInfo.Err != nil {
log.Println("error on console logs:", logInfo.Err)
ch <- logInfo.Err
return
}
// Serialize message to be sent
bytes, err := json.Marshal(serializeConsoleLogInfo(&logInfo))
if err != nil {
fmt.Println("error on json.Marshal:", err)
ch <- err
return
}
// Send Message through websocket connection
err = conn.writeMessage(websocket.TextMessage, bytes)
if err != nil {
log.Println("error writeMessage:", err)
ch <- err
return
}
}
}(ch)
return ch
}
func serializeConsoleLogInfo(l *madmin.LogInfo) (logInfo madmin.LogInfo) {
logInfo = *l
if logInfo.ConsoleMsg != "" {
if strings.HasPrefix(logInfo.ConsoleMsg, "\n") {
logInfo.ConsoleMsg = strings.TrimPrefix(logInfo.ConsoleMsg, "\n")
}
}
if logInfo.Time != "" {
logInfo.Time = getLogTime(logInfo.Time)
}
return logInfo
}
func getLogTime(lt string) string {
tm, err := time.Parse(time.RFC3339Nano, lt)
if err != nil {
return lt
}
return tm.Format(logTimeFormat)
}

View File

@@ -0,0 +1,168 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package restapi
import (
"context"
"encoding/json"
"fmt"
"testing"
"github.com/gorilla/websocket"
"github.com/minio/minio/pkg/madmin"
"github.com/stretchr/testify/assert"
)
// assigning mock at runtime instead of compile time
var minioGetLogsMock func(ctx context.Context, node string, lineCnt int, logKind string) <-chan madmin.LogInfo
// mock function of listPolicies()
func (ac adminClientMock) getLogs(ctx context.Context, node string, lineCnt int, logKind string) <-chan madmin.LogInfo {
return minioGetLogsMock(ctx, node, lineCnt, logKind)
}
func TestAdminConsoleLog(t *testing.T) {
assert := assert.New(t)
adminClient := adminClientMock{}
mockWSConn := mockConn{}
wsClientMock := wsClientMock{madmin: adminClient}
function := "startConsoleLog()"
testReceiver := make(chan madmin.LogInfo, 5)
textToReceive := "test message"
testStreamSize := 5
// Test-1: Serve Console with no errors until Console finishes sending
// define mock function behavior for minio server Console
minioGetLogsMock = func(ctx context.Context, node string, lineCnt int, logKind string) <-chan madmin.LogInfo {
ch := make(chan madmin.LogInfo)
// Only success, start a routine to start reading line by line.
go func(ch chan<- madmin.LogInfo) {
defer close(ch)
lines := make([]int, testStreamSize)
// mocking sending 5 lines of info
for range lines {
info := madmin.LogInfo{
ConsoleMsg: textToReceive,
}
ch <- info
}
}(ch)
return ch
}
// mock function of conn.ReadMessage(), no error on read
connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, nil
}
writesCount := 1
// mock connection WriteMessage() no error
connWriteMessageMock = func(messageType int, data []byte) error {
// emulate that receiver gets the message written
var t madmin.LogInfo
_ = json.Unmarshal(data, &t)
if writesCount == testStreamSize {
// for testing we need to close the receiver channel
close(testReceiver)
return nil
}
testReceiver <- t
writesCount++
return nil
}
if err := startConsoleLog(mockWSConn, wsClientMock.madmin); err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
}
// check that the TestReceiver got the same number of data from Console.
for i := range testReceiver {
assert.Equal(textToReceive, i.ConsoleMsg)
}
// Test-2: if error happens while writing, return error
connWriteMessageMock = func(messageType int, data []byte) error {
return fmt.Errorf("error on write")
}
if err := startConsoleLog(mockWSConn, wsClientMock.madmin); assert.Error(err) {
assert.Equal("error on write", err.Error())
}
// Test-3: error happens while reading, unexpected Close Error should return error.
connWriteMessageMock = func(messageType int, data []byte) error {
return nil
}
// mock function of conn.ReadMessage(), returns unexpected Close Error CloseAbnormalClosure
connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseAbnormalClosure, Text: ""}
}
if err := startConsoleLog(mockWSConn, wsClientMock.madmin); assert.Error(err) {
assert.Equal("websocket: close 1006 (abnormal closure)", err.Error())
}
// Test-4: error happens while reading, expected Close Error NormalClosure
// expected Close Error should not return an error, just end Console.
connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseNormalClosure, Text: ""}
}
if err := startConsoleLog(mockWSConn, wsClientMock.madmin); err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
}
// Test-5: error happens while reading, expected Close Error CloseGoingAway
// expected Close Error should not return an error, just return.
connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseGoingAway, Text: ""}
}
if err := startConsoleLog(mockWSConn, wsClientMock.madmin); err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
}
// Test-6: error happens while reading, non Close Error Type should be returned as
// error
connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, fmt.Errorf("error on read")
}
if err := startConsoleLog(mockWSConn, wsClientMock.madmin); assert.Error(err) {
assert.Equal("error on read", err.Error())
}
// Test-7: error happens on GetLogs Minio, Console should stop
// and error shall be returned.
minioGetLogsMock = func(ctx context.Context, node string, lineCnt int, logKind string) <-chan madmin.LogInfo {
ch := make(chan madmin.LogInfo)
// Only success, start a routine to start reading line by line.
go func(ch chan<- madmin.LogInfo) {
defer close(ch)
lines := make([]int, 2)
// mocking sending 5 lines of info
for range lines {
info := madmin.LogInfo{
ConsoleMsg: textToReceive,
}
ch <- info
}
ch <- madmin.LogInfo{Err: fmt.Errorf("error on Console")}
}(ch)
return ch
}
// mock function of conn.ReadMessage(), no error on read, should stay unless
// context is done.
connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, nil
}
if err := startConsoleLog(mockWSConn, wsClientMock.madmin); assert.Error(err) {
assert.Equal("error on Console", err.Error())
}
}

View File

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

View File

@@ -21,7 +21,6 @@ import (
"encoding/json"
"fmt"
"log"
"net"
"net/http"
"strings"
"sync"
@@ -50,37 +49,6 @@ type callStats struct {
Ttfb string `json:"timeToFirstByte"`
}
// trace serves madmin.ServiceTraceInfo
// on a Websocket connection.
func (wsc *wsClient) trace() {
defer func() {
log.Println("trace stopped")
// close connection after return
wsc.conn.close()
}()
log.Println("trace started")
err := startTraceInfo(wsc.conn, wsc.madmin)
// Send Connection Close Message indicating the Status Code
// see https://tools.ietf.org/html/rfc6455#page-45
if err != nil {
// If connection exceeded read deadline send Close
// Message Policy Violation code since we don't want
// to let the receiver figure out the read deadline.
// This is a generic code designed if there is a
// need to hide specific details about the policy.
if nErr, ok := err.(net.Error); ok && nErr.Timeout() {
wsc.conn.writeMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.ClosePolicyViolation, ""))
return
}
// else, internal server error
wsc.conn.writeMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseInternalServerErr, err.Error()))
return
}
// normal closure
wsc.conn.writeMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
}
// startTraceInfo starts trace of the servers
// by first setting a websocket reader that will
// check for a heartbeat.
@@ -115,7 +83,7 @@ func startTraceInfo(conn WSConn, client MinioAdmin) (mError error) {
cancel()
}(&wg)
// wait for traceCh to finish
// get traceCh error on finish
if err := <-traceCh; err != nil {
mError = err
}

View File

@@ -81,8 +81,11 @@ type MinioAdmin interface {
startProfiling(ctx context.Context, profiler madmin.ProfilerType) ([]madmin.StartProfilingResult, error)
stopProfiling(ctx context.Context) (io.ReadCloser, error)
serviceTrace(ctx context.Context, allTrace, errTrace bool) <-chan madmin.ServiceTraceInfo
getLogs(ctx context.Context, node string, lineCnt int, logKind string) <-chan madmin.LogInfo
// Service Accounts
addServiceAccount(ctx context.Context, policy *iampolicy.Policy) (mauth.Credentials, error)
listServiceAccounts(ctx context.Context) (madmin.ListServiceAccountsResp, error)
deleteServiceAccount(ctx context.Context, serviceAccount string) error
}
// Interface implementation
@@ -203,16 +206,40 @@ func (ac adminClient) serviceTrace(ctx context.Context, allTrace, errTrace bool)
return ac.client.ServiceTrace(ctx, allTrace, errTrace)
}
// implements madmin.GetLogs()
func (ac adminClient) getLogs(ctx context.Context, node string, lineCnt int, logKind string) <-chan madmin.LogInfo {
return ac.client.GetLogs(ctx, node, lineCnt, logKind)
}
// implements madmin.AddServiceAccount()
func (ac adminClient) addServiceAccount(ctx context.Context, policy *iampolicy.Policy) (mauth.Credentials, error) {
return ac.client.AddServiceAccount(ctx, policy)
}
// implements madmin.ListServiceAccounts()
func (ac adminClient) listServiceAccounts(ctx context.Context) (madmin.ListServiceAccountsResp, error) {
return ac.client.ListServiceAccounts(ctx)
}
// implements madmin.DeleteServiceAccount()
func (ac adminClient) deleteServiceAccount(ctx context.Context, serviceAccount string) error {
return ac.client.DeleteServiceAccount(ctx, serviceAccount)
}
func newMAdminClient(jwt string) (*madmin.AdminClient, error) {
claims, err := auth.JWTAuthenticate(jwt)
if err != nil {
return nil, err
}
adminClient, err := newAdminFromClaims(claims)
if err != nil {
return nil, err
}
return adminClient, nil
}
// newAdminFromClaims creates a minio admin from Decrypted claims using Assume role credentials
func newAdminFromClaims(claims *auth.DecryptedClaims) (*madmin.AdminClient, error) {
adminClient, err := madmin.NewWithOptions(getMinIOEndpoint(), &madmin.Options{
Creds: credentials.NewStaticV4(claims.AccessKeyID, claims.SecretAccessKey, claims.SessionToken),
Secure: getMinIOEndpointIsSecure(),

View File

@@ -110,7 +110,7 @@ func (c mcS3Client) removeNotificationConfig(arn string, event string, prefix st
return c.client.RemoveNotificationConfig(arn, event, prefix, suffix)
}
// Define MCSCredentials interface with all functions to be implemented
// MCSCredentials interface with all functions to be implemented
// by mock when testing, it should include all needed minioCredentials.Credentials api calls
// that are used within this project.
type MCSCredentials interface {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,90 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package user_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"net/http"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/mcs/models"
)
// DeleteServiceAccountHandlerFunc turns a function with the right signature into a delete service account handler
type DeleteServiceAccountHandlerFunc func(DeleteServiceAccountParams, *models.Principal) middleware.Responder
// Handle executing the request and returning a response
func (fn DeleteServiceAccountHandlerFunc) Handle(params DeleteServiceAccountParams, principal *models.Principal) middleware.Responder {
return fn(params, principal)
}
// DeleteServiceAccountHandler interface for that can handle valid delete service account params
type DeleteServiceAccountHandler interface {
Handle(DeleteServiceAccountParams, *models.Principal) middleware.Responder
}
// NewDeleteServiceAccount creates a new http.Handler for the delete service account operation
func NewDeleteServiceAccount(ctx *middleware.Context, handler DeleteServiceAccountHandler) *DeleteServiceAccount {
return &DeleteServiceAccount{Context: ctx, Handler: handler}
}
/*DeleteServiceAccount swagger:route DELETE /service-accounts/{access_key} UserAPI deleteServiceAccount
Delete Service Account
*/
type DeleteServiceAccount struct {
Context *middleware.Context
Handler DeleteServiceAccountHandler
}
func (o *DeleteServiceAccount) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
route, rCtx, _ := o.Context.RouteInfo(r)
if rCtx != nil {
r = rCtx
}
var Params = NewDeleteServiceAccountParams()
uprinc, aCtx, err := o.Context.Authorize(r, route)
if err != nil {
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
if aCtx != nil {
r = aCtx
}
var principal *models.Principal
if uprinc != nil {
principal = uprinc.(*models.Principal) // this is really a models.Principal, I promise
}
if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
res := o.Handler.Handle(Params, principal) // actually handle the request
o.Context.Respond(rw, r, route.Produces, route, res)
}

View File

@@ -0,0 +1,89 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package user_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"net/http"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/strfmt"
)
// NewDeleteServiceAccountParams creates a new DeleteServiceAccountParams object
// no default values defined in spec.
func NewDeleteServiceAccountParams() DeleteServiceAccountParams {
return DeleteServiceAccountParams{}
}
// DeleteServiceAccountParams contains all the bound params for the delete service account operation
// typically these are obtained from a http.Request
//
// swagger:parameters DeleteServiceAccount
type DeleteServiceAccountParams struct {
// HTTP Request Object
HTTPRequest *http.Request `json:"-"`
/*
Required: true
In: path
*/
AccessKey string
}
// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
// for simple values it will use straight method calls.
//
// To ensure default values, the struct must have been initialized with NewDeleteServiceAccountParams() beforehand.
func (o *DeleteServiceAccountParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {
var res []error
o.HTTPRequest = r
rAccessKey, rhkAccessKey, _ := route.Params.GetOK("access_key")
if err := o.bindAccessKey(rAccessKey, rhkAccessKey, route.Formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
// bindAccessKey binds and validates parameter AccessKey from path.
func (o *DeleteServiceAccountParams) bindAccessKey(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: true
// Parameter is provided by construction from the route
o.AccessKey = raw
return nil
}

View File

@@ -0,0 +1,113 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package user_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"net/http"
"github.com/go-openapi/runtime"
"github.com/minio/mcs/models"
)
// DeleteServiceAccountNoContentCode is the HTTP code returned for type DeleteServiceAccountNoContent
const DeleteServiceAccountNoContentCode int = 204
/*DeleteServiceAccountNoContent A successful response.
swagger:response deleteServiceAccountNoContent
*/
type DeleteServiceAccountNoContent struct {
}
// NewDeleteServiceAccountNoContent creates DeleteServiceAccountNoContent with default headers values
func NewDeleteServiceAccountNoContent() *DeleteServiceAccountNoContent {
return &DeleteServiceAccountNoContent{}
}
// WriteResponse to the client
func (o *DeleteServiceAccountNoContent) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.Header().Del(runtime.HeaderContentType) //Remove Content-Type on empty responses
rw.WriteHeader(204)
}
/*DeleteServiceAccountDefault Generic error response.
swagger:response deleteServiceAccountDefault
*/
type DeleteServiceAccountDefault struct {
_statusCode int
/*
In: Body
*/
Payload *models.Error `json:"body,omitempty"`
}
// NewDeleteServiceAccountDefault creates DeleteServiceAccountDefault with default headers values
func NewDeleteServiceAccountDefault(code int) *DeleteServiceAccountDefault {
if code <= 0 {
code = 500
}
return &DeleteServiceAccountDefault{
_statusCode: code,
}
}
// WithStatusCode adds the status to the delete service account default response
func (o *DeleteServiceAccountDefault) WithStatusCode(code int) *DeleteServiceAccountDefault {
o._statusCode = code
return o
}
// SetStatusCode sets the status to the delete service account default response
func (o *DeleteServiceAccountDefault) SetStatusCode(code int) {
o._statusCode = code
}
// WithPayload adds the payload to the delete service account default response
func (o *DeleteServiceAccountDefault) WithPayload(payload *models.Error) *DeleteServiceAccountDefault {
o.Payload = payload
return o
}
// SetPayload sets the payload to the delete service account default response
func (o *DeleteServiceAccountDefault) SetPayload(payload *models.Error) {
o.Payload = payload
}
// WriteResponse to the client
func (o *DeleteServiceAccountDefault) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.WriteHeader(o._statusCode)
if o.Payload != nil {
payload := o.Payload
if err := producer.Produce(rw, payload); err != nil {
panic(err) // let the recovery middleware deal with this
}
}
}

View File

@@ -0,0 +1,116 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package user_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"errors"
"net/url"
golangswaggerpaths "path"
"strings"
)
// DeleteServiceAccountURL generates an URL for the delete service account operation
type DeleteServiceAccountURL struct {
AccessKey string
_basePath string
// avoid unkeyed usage
_ struct{}
}
// WithBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *DeleteServiceAccountURL) WithBasePath(bp string) *DeleteServiceAccountURL {
o.SetBasePath(bp)
return o
}
// SetBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *DeleteServiceAccountURL) SetBasePath(bp string) {
o._basePath = bp
}
// Build a url path and query string
func (o *DeleteServiceAccountURL) Build() (*url.URL, error) {
var _result url.URL
var _path = "/service-accounts/{access_key}"
accessKey := o.AccessKey
if accessKey != "" {
_path = strings.Replace(_path, "{access_key}", accessKey, -1)
} else {
return nil, errors.New("accessKey is required on DeleteServiceAccountURL")
}
_basePath := o._basePath
if _basePath == "" {
_basePath = "/api/v1"
}
_result.Path = golangswaggerpaths.Join(_basePath, _path)
return &_result, nil
}
// Must is a helper function to panic when the url builder returns an error
func (o *DeleteServiceAccountURL) Must(u *url.URL, err error) *url.URL {
if err != nil {
panic(err)
}
if u == nil {
panic("url can't be nil")
}
return u
}
// String returns the string representation of the path with query string
func (o *DeleteServiceAccountURL) String() string {
return o.Must(o.Build()).String()
}
// BuildFull builds a full url with scheme, host, path and query string
func (o *DeleteServiceAccountURL) BuildFull(scheme, host string) (*url.URL, error) {
if scheme == "" {
return nil, errors.New("scheme is required for a full url on DeleteServiceAccountURL")
}
if host == "" {
return nil, errors.New("host is required for a full url on DeleteServiceAccountURL")
}
base, err := o.Build()
if err != nil {
return nil, err
}
base.Scheme = scheme
base.Host = host
return base, nil
}
// StringFull returns the string representation of a complete url
func (o *DeleteServiceAccountURL) StringFull(scheme, host string) string {
return o.Must(o.BuildFull(scheme, host)).String()
}

View File

@@ -0,0 +1,90 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package user_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"net/http"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/mcs/models"
)
// ListUserServiceAccountsHandlerFunc turns a function with the right signature into a list user service accounts handler
type ListUserServiceAccountsHandlerFunc func(ListUserServiceAccountsParams, *models.Principal) middleware.Responder
// Handle executing the request and returning a response
func (fn ListUserServiceAccountsHandlerFunc) Handle(params ListUserServiceAccountsParams, principal *models.Principal) middleware.Responder {
return fn(params, principal)
}
// ListUserServiceAccountsHandler interface for that can handle valid list user service accounts params
type ListUserServiceAccountsHandler interface {
Handle(ListUserServiceAccountsParams, *models.Principal) middleware.Responder
}
// NewListUserServiceAccounts creates a new http.Handler for the list user service accounts operation
func NewListUserServiceAccounts(ctx *middleware.Context, handler ListUserServiceAccountsHandler) *ListUserServiceAccounts {
return &ListUserServiceAccounts{Context: ctx, Handler: handler}
}
/*ListUserServiceAccounts swagger:route GET /service-accounts UserAPI listUserServiceAccounts
List User's Service Accounts
*/
type ListUserServiceAccounts struct {
Context *middleware.Context
Handler ListUserServiceAccountsHandler
}
func (o *ListUserServiceAccounts) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
route, rCtx, _ := o.Context.RouteInfo(r)
if rCtx != nil {
r = rCtx
}
var Params = NewListUserServiceAccountsParams()
uprinc, aCtx, err := o.Context.Authorize(r, route)
if err != nil {
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
if aCtx != nil {
r = aCtx
}
var principal *models.Principal
if uprinc != nil {
principal = uprinc.(*models.Principal) // this is really a models.Principal, I promise
}
if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
res := o.Handler.Handle(Params, principal) // actually handle the request
o.Context.Respond(rw, r, route.Produces, route, res)
}

View File

@@ -0,0 +1,130 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package user_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"net/http"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// NewListUserServiceAccountsParams creates a new ListUserServiceAccountsParams object
// no default values defined in spec.
func NewListUserServiceAccountsParams() ListUserServiceAccountsParams {
return ListUserServiceAccountsParams{}
}
// ListUserServiceAccountsParams contains all the bound params for the list user service accounts operation
// typically these are obtained from a http.Request
//
// swagger:parameters ListUserServiceAccounts
type ListUserServiceAccountsParams struct {
// HTTP Request Object
HTTPRequest *http.Request `json:"-"`
/*
In: query
*/
Limit *int32
/*
In: query
*/
Offset *int32
}
// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
// for simple values it will use straight method calls.
//
// To ensure default values, the struct must have been initialized with NewListUserServiceAccountsParams() beforehand.
func (o *ListUserServiceAccountsParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {
var res []error
o.HTTPRequest = r
qs := runtime.Values(r.URL.Query())
qLimit, qhkLimit, _ := qs.GetOK("limit")
if err := o.bindLimit(qLimit, qhkLimit, route.Formats); err != nil {
res = append(res, err)
}
qOffset, qhkOffset, _ := qs.GetOK("offset")
if err := o.bindOffset(qOffset, qhkOffset, route.Formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
// bindLimit binds and validates parameter Limit from query.
func (o *ListUserServiceAccountsParams) bindLimit(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: false
// AllowEmptyValue: false
if raw == "" { // empty values pass all other validations
return nil
}
value, err := swag.ConvertInt32(raw)
if err != nil {
return errors.InvalidType("limit", "query", "int32", raw)
}
o.Limit = &value
return nil
}
// bindOffset binds and validates parameter Offset from query.
func (o *ListUserServiceAccountsParams) bindOffset(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: false
// AllowEmptyValue: false
if raw == "" { // empty values pass all other validations
return nil
}
value, err := swag.ConvertInt32(raw)
if err != nil {
return errors.InvalidType("offset", "query", "int32", raw)
}
o.Offset = &value
return nil
}

View File

@@ -0,0 +1,136 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package user_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"net/http"
"github.com/go-openapi/runtime"
"github.com/minio/mcs/models"
)
// ListUserServiceAccountsOKCode is the HTTP code returned for type ListUserServiceAccountsOK
const ListUserServiceAccountsOKCode int = 200
/*ListUserServiceAccountsOK A successful response.
swagger:response listUserServiceAccountsOK
*/
type ListUserServiceAccountsOK struct {
/*
In: Body
*/
Payload models.ServiceAccounts `json:"body,omitempty"`
}
// NewListUserServiceAccountsOK creates ListUserServiceAccountsOK with default headers values
func NewListUserServiceAccountsOK() *ListUserServiceAccountsOK {
return &ListUserServiceAccountsOK{}
}
// WithPayload adds the payload to the list user service accounts o k response
func (o *ListUserServiceAccountsOK) WithPayload(payload models.ServiceAccounts) *ListUserServiceAccountsOK {
o.Payload = payload
return o
}
// SetPayload sets the payload to the list user service accounts o k response
func (o *ListUserServiceAccountsOK) SetPayload(payload models.ServiceAccounts) {
o.Payload = payload
}
// WriteResponse to the client
func (o *ListUserServiceAccountsOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.WriteHeader(200)
payload := o.Payload
if payload == nil {
// return empty array
payload = models.ServiceAccounts{}
}
if err := producer.Produce(rw, payload); err != nil {
panic(err) // let the recovery middleware deal with this
}
}
/*ListUserServiceAccountsDefault Generic error response.
swagger:response listUserServiceAccountsDefault
*/
type ListUserServiceAccountsDefault struct {
_statusCode int
/*
In: Body
*/
Payload *models.Error `json:"body,omitempty"`
}
// NewListUserServiceAccountsDefault creates ListUserServiceAccountsDefault with default headers values
func NewListUserServiceAccountsDefault(code int) *ListUserServiceAccountsDefault {
if code <= 0 {
code = 500
}
return &ListUserServiceAccountsDefault{
_statusCode: code,
}
}
// WithStatusCode adds the status to the list user service accounts default response
func (o *ListUserServiceAccountsDefault) WithStatusCode(code int) *ListUserServiceAccountsDefault {
o._statusCode = code
return o
}
// SetStatusCode sets the status to the list user service accounts default response
func (o *ListUserServiceAccountsDefault) SetStatusCode(code int) {
o._statusCode = code
}
// WithPayload adds the payload to the list user service accounts default response
func (o *ListUserServiceAccountsDefault) WithPayload(payload *models.Error) *ListUserServiceAccountsDefault {
o.Payload = payload
return o
}
// SetPayload sets the payload to the list user service accounts default response
func (o *ListUserServiceAccountsDefault) SetPayload(payload *models.Error) {
o.Payload = payload
}
// WriteResponse to the client
func (o *ListUserServiceAccountsDefault) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.WriteHeader(o._statusCode)
if o.Payload != nil {
payload := o.Payload
if err := producer.Produce(rw, payload); err != nil {
panic(err) // let the recovery middleware deal with this
}
}
}

View File

@@ -0,0 +1,131 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package user_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"errors"
"net/url"
golangswaggerpaths "path"
"github.com/go-openapi/swag"
)
// ListUserServiceAccountsURL generates an URL for the list user service accounts operation
type ListUserServiceAccountsURL struct {
Limit *int32
Offset *int32
_basePath string
// avoid unkeyed usage
_ struct{}
}
// WithBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *ListUserServiceAccountsURL) WithBasePath(bp string) *ListUserServiceAccountsURL {
o.SetBasePath(bp)
return o
}
// SetBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *ListUserServiceAccountsURL) SetBasePath(bp string) {
o._basePath = bp
}
// Build a url path and query string
func (o *ListUserServiceAccountsURL) Build() (*url.URL, error) {
var _result url.URL
var _path = "/service-accounts"
_basePath := o._basePath
if _basePath == "" {
_basePath = "/api/v1"
}
_result.Path = golangswaggerpaths.Join(_basePath, _path)
qs := make(url.Values)
var limitQ string
if o.Limit != nil {
limitQ = swag.FormatInt32(*o.Limit)
}
if limitQ != "" {
qs.Set("limit", limitQ)
}
var offsetQ string
if o.Offset != nil {
offsetQ = swag.FormatInt32(*o.Offset)
}
if offsetQ != "" {
qs.Set("offset", offsetQ)
}
_result.RawQuery = qs.Encode()
return &_result, nil
}
// Must is a helper function to panic when the url builder returns an error
func (o *ListUserServiceAccountsURL) Must(u *url.URL, err error) *url.URL {
if err != nil {
panic(err)
}
if u == nil {
panic("url can't be nil")
}
return u
}
// String returns the string representation of the path with query string
func (o *ListUserServiceAccountsURL) String() string {
return o.Must(o.Build()).String()
}
// BuildFull builds a full url with scheme, host, path and query string
func (o *ListUserServiceAccountsURL) BuildFull(scheme, host string) (*url.URL, error) {
if scheme == "" {
return nil, errors.New("scheme is required for a full url on ListUserServiceAccountsURL")
}
if host == "" {
return nil, errors.New("host is required for a full url on ListUserServiceAccountsURL")
}
base, err := o.Build()
if err != nil {
return nil, err
}
base.Scheme = scheme
base.Host = host
return base, nil
}
// StringFull returns the string representation of a complete url
func (o *ListUserServiceAccountsURL) StringFull(scheme, host string) string {
return o.Must(o.BuildFull(scheme, host)).String()
}

View File

@@ -31,6 +31,10 @@ import (
"github.com/minio/mcs/restapi/operations/user_api"
)
var (
errorGeneric = errors.New("an error occurred, please try again")
)
func registerLoginHandlers(api *operations.McsAPI) {
// get login strategy
api.UserAPILoginDetailHandler = user_api.LoginDetailHandlerFunc(func(params user_api.LoginDetailParams) middleware.Responder {
@@ -74,9 +78,32 @@ func login(credentials MCSCredentials) (*string, error) {
return &jwt, nil
}
func getConfiguredRegion(client MinioAdmin) string {
location := ""
configuration, err := getConfig(client, "region")
if err != nil {
log.Println("error obtaining MinIO region:", err)
return location
}
// region is an array of 1 element
if len(configuration) > 0 {
location = configuration[0].Value
}
return location
}
// getLoginResponse performs login() and serializes it to the handler's output
func getLoginResponse(lr *models.LoginRequest) (*models.LoginResponse, error) {
creds, err := newMcsCredentials(*lr.AccessKey, *lr.SecretKey, "")
mAdmin, err := newSuperMAdminClient()
if err != nil {
log.Println("error creating Madmin Client:", err)
return nil, errorGeneric
}
adminClient := adminClient{client: mAdmin}
// obtain the configured MinIO region
// need it for user authentication
location := getConfiguredRegion(adminClient)
creds, err := newMcsCredentials(*lr.AccessKey, *lr.SecretKey, location)
if err != nil {
log.Println("error login:", err)
return nil, err
@@ -131,27 +158,32 @@ func getLoginOauth2AuthResponse(lr *models.LoginOauth2AuthRequest) (*models.Logi
// initialize new oauth2 client
oauth2Client, err := oauth2.NewOauth2ProviderClient(ctx, nil)
if err != nil {
return nil, err
log.Println("error getting new oauth2 client:", err)
return nil, errorGeneric
}
// initialize new identity provider
identityProvider := &auth.IdentityProvider{Client: oauth2Client}
// Validate user against IDP
identity, err := loginOauth2Auth(ctx, identityProvider, *lr.Code, *lr.State)
if err != nil {
return nil, err
log.Println("error validating user identity against idp:", err)
return nil, errorGeneric
}
mAdmin, err := newSuperMAdminClient()
if err != nil {
log.Println("error creating Madmin Client:", err)
return nil, err
return nil, errorGeneric
}
adminClient := adminClient{client: mAdmin}
accessKey := identity.Email
secretKey := utils.RandomCharString(32)
// Create user in MinIO
// obtain the configured MinIO region
// need it for user authentication
location := getConfiguredRegion(adminClient)
// create user in MinIO
if _, err := addUser(ctx, adminClient, &accessKey, &secretKey, []string{}); err != nil {
log.Println("error adding user:", err)
return nil, err
return nil, errorGeneric
}
// rollback user if there's an error after this point
defer func() {
@@ -164,19 +196,19 @@ func getLoginOauth2AuthResponse(lr *models.LoginOauth2AuthRequest) (*models.Logi
// assign the "mcsAdmin" policy to this user
if err := setPolicy(ctx, adminClient, oauth2.GetIDPPolicyForUser(), accessKey, models.PolicyEntityUser); err != nil {
log.Println("error setting policy:", err)
return nil, err
return nil, errorGeneric
}
// User was created correctly, create a new session/JWT
creds, err := newMcsCredentials(accessKey, secretKey, "")
creds, err := newMcsCredentials(accessKey, secretKey, location)
if err != nil {
log.Println("error login:", err)
return nil, err
return nil, errorGeneric
}
credentials := mcsCredentials{minioCredentials: creds}
jwt, err := login(credentials)
if err != nil {
log.Println("error login:", err)
return nil, err
return nil, errorGeneric
}
// serialize output
loginResponse := &models.LoginResponse{
@@ -184,5 +216,5 @@ func getLoginOauth2AuthResponse(lr *models.LoginOauth2AuthRequest) (*models.Logi
}
return loginResponse, nil
}
return nil, errors.New("an error occurred, please try again")
return nil, errorGeneric
}

View File

@@ -24,6 +24,8 @@ import (
"github.com/minio/mcs/pkg/auth"
"github.com/minio/mcs/pkg/auth/idp/oauth2"
"github.com/minio/minio-go/v6/pkg/credentials"
"github.com/minio/minio/cmd/config"
"github.com/minio/minio/pkg/madmin"
"github.com/stretchr/testify/assert"
)
@@ -101,3 +103,95 @@ func TestLoginOauth2Auth(t *testing.T) {
funcAssert.Equal("error", err.Error())
}
}
func Test_getConfiguredRegion(t *testing.T) {
client := adminClientMock{}
type args struct {
client adminClientMock
}
tests := []struct {
name string
args args
want string
mock func()
}{
// If MinIO returns an error, we return empty region name
{
name: "region",
args: args{
client: client,
},
want: "",
mock: func() {
// mock function response from getConfig()
minioGetConfigKVMock = func(key string) ([]byte, error) {
return nil, errors.New("Invalid config")
}
// mock function response from listConfig()
minioHelpConfigKVMock = func(subSys, key string, envOnly bool) (madmin.Help, error) {
return madmin.Help{}, errors.New("no help")
}
},
},
// MinIO returns an empty region name
{
name: "region",
args: args{
client: client,
},
want: "",
mock: func() {
// mock function response from getConfig()
minioGetConfigKVMock = func(key string) ([]byte, error) {
return []byte("region name= "), nil
}
// mock function response from listConfig()
minioHelpConfigKVMock = func(subSys, key string, envOnly bool) (madmin.Help, error) {
return madmin.Help{
SubSys: config.RegionSubSys,
Description: "label the location of the server",
MultipleTargets: false,
KeysHelp: []madmin.HelpKV{
{
Key: "name",
Description: "name of the location of the server e.g. \"us-west-rack2\"",
Optional: true,
Type: "string",
MultipleTargets: false,
},
{
Key: "comment",
Description: "optionally add a comment to this setting",
Optional: true,
Type: "sentence",
MultipleTargets: false,
},
},
}, nil
}
},
},
// MinIO returns the asia region
{
name: "region",
args: args{
client: client,
},
want: "asia",
mock: func() {
minioGetConfigKVMock = func(key string) ([]byte, error) {
return []byte("region name=asia "), nil
}
},
},
}
for _, tt := range tests {
tt.mock()
t.Run(tt.name, func(t *testing.T) {
if got := getConfiguredRegion(tt.args.client); got != tt.want {
t.Errorf("getConfiguredRegion() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -41,6 +41,24 @@ func registerServiceAccountsHandlers(api *operations.McsAPI) {
}
return user_api.NewCreateServiceAccountCreated().WithPayload(creds)
})
// List Service Accounts for User
api.UserAPIListUserServiceAccountsHandler = user_api.ListUserServiceAccountsHandlerFunc(func(params user_api.ListUserServiceAccountsParams, principal *models.Principal) middleware.Responder {
sessionID := string(*principal)
serviceAccounts, err := getUserServiceAccountsResponse(sessionID)
if err != nil {
return user_api.NewListUserServiceAccountsDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
return user_api.NewListUserServiceAccountsOK().WithPayload(serviceAccounts)
})
// Delete a User's service account
api.UserAPIDeleteServiceAccountHandler = user_api.DeleteServiceAccountHandlerFunc(func(params user_api.DeleteServiceAccountParams, principal *models.Principal) middleware.Responder {
sessionID := string(*principal)
if err := getDeleteServiceAccountResponse(sessionID, params.AccessKey); err != nil {
return user_api.NewDeleteServiceAccountDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
return user_api.NewDeleteServiceAccountNoContent()
})
}
// createServiceAccount adds a service account to the userClient and assigns a policy to him if defined.
@@ -64,7 +82,7 @@ func createServiceAccount(ctx context.Context, userClient MinioAdmin, policy str
// getCreateServiceAccountResponse creates a service account with the defined policy for the user that
// is requestingit ,it first gets the credentials of the user and creates a client which is going to
// make the call to create the Service Account
func getCreateServiceAccountResponse(userSessionID string, serviceAccount *models.ServiceAccount) (*models.ServiceAccountCreds, error) {
func getCreateServiceAccountResponse(userSessionID string, serviceAccount *models.ServiceAccountRequest) (*models.ServiceAccountCreds, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
defer cancel()
@@ -84,3 +102,66 @@ func getCreateServiceAccountResponse(userSessionID string, serviceAccount *model
}
return saCreds, nil
}
// getUserServiceAccount gets list of the user's service accounts
func getUserServiceAccounts(ctx context.Context, userClient MinioAdmin) (models.ServiceAccounts, error) {
listServAccs, err := userClient.listServiceAccounts(ctx)
if err != nil {
return nil, err
}
serviceAccounts := models.ServiceAccounts{}
for _, acc := range listServAccs.Accounts {
serviceAccounts = append(serviceAccounts, acc)
}
return serviceAccounts, nil
}
// getUserServiceAccountsResponse authenticates the user and calls
// getUserServiceAccounts to list the user's service accounts
func getUserServiceAccountsResponse(userSessionID string) (models.ServiceAccounts, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
defer cancel()
userAdmin, err := newMAdminClient(userSessionID)
if err != nil {
log.Println("error creating user Client:", err)
return nil, err
}
// create a MinIO user Admin Client interface implementation
// defining the client to be used
userAdminClient := adminClient{client: userAdmin}
serviceAccounts, err := getUserServiceAccounts(ctx, userAdminClient)
if err != nil {
log.Println("error listing user's service account:", err)
return nil, err
}
return serviceAccounts, nil
}
// deleteServiceAccount calls delete service account api
func deleteServiceAccount(ctx context.Context, userClient MinioAdmin, accessKey string) error {
return userClient.deleteServiceAccount(ctx, accessKey)
}
// getDeleteServiceAccountResponse authenticates the user and calls deleteServiceAccount
func getDeleteServiceAccountResponse(userSessionID, accessKey string) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
defer cancel()
userAdmin, err := newMAdminClient(userSessionID)
if err != nil {
log.Println("error creating user Client:", err)
return err
}
// create a MinIO user Admin Client interface implementation
// defining the client to be used
userAdminClient := adminClient{client: userAdmin}
if err := deleteServiceAccount(ctx, userAdminClient, accessKey); err != nil {
log.Println("error deleting user's service account:", err)
return err
}
return nil
}

View File

@@ -25,17 +25,30 @@ import (
"github.com/minio/minio/pkg/auth"
iampolicy "github.com/minio/minio/pkg/iam/policy"
"github.com/minio/minio/pkg/madmin"
"github.com/stretchr/testify/assert"
)
// assigning mock at runtime instead of compile time
var minioAddServiceAccountMock func(ctx context.Context, policy *iampolicy.Policy) (auth.Credentials, error)
var minioListServiceAccountsMock func(ctx context.Context) (madmin.ListServiceAccountsResp, error)
var minioDeleteServiceAccountMock func(ctx context.Context, serviceAccount string) error
// mock function of listUsers()
// mock function of AddServiceAccount()
func (ac adminClientMock) addServiceAccount(ctx context.Context, policy *iampolicy.Policy) (auth.Credentials, error) {
return minioAddServiceAccountMock(ctx, policy)
}
// mock function of ListServiceAccounts()
func (ac adminClientMock) listServiceAccounts(ctx context.Context) (madmin.ListServiceAccountsResp, error) {
return minioListServiceAccountsMock(ctx)
}
// mock function of DeleteServiceAccount()
func (ac adminClientMock) deleteServiceAccount(ctx context.Context, serviceAccount string) error {
return minioDeleteServiceAccountMock(ctx, serviceAccount)
}
func TestAddServiceAccount(t *testing.T) {
assert := assert.New(t)
// mock minIO client
@@ -84,3 +97,61 @@ func TestAddServiceAccount(t *testing.T) {
assert.Equal("error", err.Error())
}
}
func TestListServiceAccounts(t *testing.T) {
assert := assert.New(t)
// mock minIO client
client := adminClientMock{}
function := "getUserServiceAccounts()"
// Test-1: getUserServiceAccounts list serviceaccounts for a user
ctx := context.Background()
mockResponse := madmin.ListServiceAccountsResp{
Accounts: []string{"accesskey1", "accesskey2"},
}
minioListServiceAccountsMock = func(ctx context.Context) (madmin.ListServiceAccountsResp, error) {
return mockResponse, nil
}
serviceAccounts, err := getUserServiceAccounts(ctx, client)
if err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
}
for i, sa := range serviceAccounts {
assert.Equal(mockResponse.Accounts[i], sa)
}
// Test-2: getUserServiceAccounts returns an error, handle it properly
minioListServiceAccountsMock = func(ctx context.Context) (madmin.ListServiceAccountsResp, error) {
return madmin.ListServiceAccountsResp{}, errors.New("error")
}
_, err = getUserServiceAccounts(ctx, client)
if assert.Error(err) {
assert.Equal("error", err.Error())
}
}
func TestDeleteServiceAccount(t *testing.T) {
assert := assert.New(t)
// mock minIO client
client := adminClientMock{}
function := "deleteServiceAccount()"
ctx := context.Background()
// Test-1: deleteServiceAccount receive a service account to delete
testServiceAccount := "accesskeytest"
minioDeleteServiceAccountMock = func(ctx context.Context, serviceAccount string) error {
return nil
}
if err := deleteServiceAccount(ctx, client, testServiceAccount); err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
}
// Test-2: if an invalid policy is assigned to the service account, this will raise an error
minioDeleteServiceAccountMock = func(ctx context.Context, serviceAccount string) error {
return errors.New("error")
}
if err := deleteServiceAccount(ctx, client, testServiceAccount); assert.Error(err) {
assert.Equal("error", err.Error())
}
}

View File

@@ -19,6 +19,7 @@ package restapi
import (
"context"
"log"
"net"
"net/http"
"strings"
"sync"
@@ -108,9 +109,8 @@ func (c wsConn) readMessage() (messageType int, p []byte, err error) {
//
// TODO: Enable CORS
func serveWS(w http.ResponseWriter, req *http.Request) {
// authenticate WS connection
// TODO: use this claims to create the adminClient
_, err := ws.Authenticate(req)
// authenticate WS connection with MCS
claims, err := ws.Authenticate(req)
if err != nil {
log.Print("error on ws authentication: ", err)
errors.ServeError(w, req, err)
@@ -125,13 +125,9 @@ func serveWS(w http.ResponseWriter, req *http.Request) {
return
}
// TODO: CHANGE ! to use newMAdminClient once Assume Role is
// allowed to do Trace use jwt on ws.
// Using newSuperMAdminClient in the meantime for sake of functionality
// Only start Websocket Interaction after user has been
// authenticated.
mAdmin, err := newSuperMAdminClient()
// authenticated with MinIO
mAdmin, err := newAdminFromClaims(claims)
if err != nil {
log.Println("error creating Madmin Client:", err)
errors.ServeError(w, req, err)
@@ -149,6 +145,8 @@ func serveWS(w http.ResponseWriter, req *http.Request) {
switch strings.TrimPrefix(req.URL.Path, wsBasePath) {
case "/trace":
go wsClient.trace()
case "/console":
go wsClient.console()
default:
// path not found
conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
@@ -205,3 +203,65 @@ func wsReadCheck(ctx context.Context, wg *sync.WaitGroup, conn WSConn) chan erro
}(ch)
return ch
}
// trace serves madmin.ServiceTraceInfo
// on a Websocket connection.
func (wsc *wsClient) trace() {
defer func() {
log.Println("trace stopped")
// close connection after return
wsc.conn.close()
}()
log.Println("trace started")
err := startTraceInfo(wsc.conn, wsc.madmin)
// Send Connection Close Message indicating the Status Code
// see https://tools.ietf.org/html/rfc6455#page-45
if err != nil {
// If connection exceeded read deadline send Close
// Message Policy Violation code since we don't want
// to let the receiver figure out the read deadline.
// This is a generic code designed if there is a
// need to hide specific details about the policy.
if nErr, ok := err.(net.Error); ok && nErr.Timeout() {
wsc.conn.writeMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.ClosePolicyViolation, ""))
return
}
// else, internal server error
wsc.conn.writeMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseInternalServerErr, err.Error()))
return
}
// normal closure
wsc.conn.writeMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
}
// console serves madmin.GetLogs
// on a Websocket connection.
func (wsc *wsClient) console() {
defer func() {
log.Println("console logs stopped")
// close connection after return
wsc.conn.close()
}()
log.Println("console logs started")
err := startConsoleLog(wsc.conn, wsc.madmin)
// Send Connection Close Message indicating the Status Code
// see https://tools.ietf.org/html/rfc6455#page-45
if err != nil {
// If connection exceeded read deadline send Close
// Message Policy Violation code since we don't want
// to let the receiver figure out the read deadline.
// This is a generic code designed if there is a
// need to hide specific details about the policy.
if nErr, ok := err.(net.Error); ok && nErr.Timeout() {
wsc.conn.writeMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.ClosePolicyViolation, ""))
return
}
// else, internal server error
wsc.conn.writeMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseInternalServerErr, err.Error()))
return
}
// normal closure
wsc.conn.writeMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
}

View File

@@ -309,6 +309,31 @@ paths:
- UserAPI
/service-accounts:
get:
summary: List User's Service Accounts
operationId: ListUserServiceAccounts
parameters:
- name: offset
in: query
required: false
type: integer
format: int32
- name: limit
in: query
required: false
type: integer
format: int32
responses:
200:
description: A successful response.
schema:
$ref: "#/definitions/serviceAccounts"
default:
description: Generic error response.
schema:
$ref: "#/definitions/error"
tags:
- UserAPI
post:
summary: Create Service Account
operationId: CreateServiceAccount
@@ -317,7 +342,7 @@ paths:
in: body
required: true
schema:
$ref: '#/definitions/serviceAccount'
$ref: '#/definitions/serviceAccountRequest'
responses:
201:
description: A successful response.
@@ -329,6 +354,25 @@ paths:
$ref: "#/definitions/error"
tags:
- UserAPI
/service-accounts/{access_key}:
delete:
summary: Delete Service Account
operationId: DeleteServiceAccount
parameters:
- name: access_key
in: path
required: true
type: string
responses:
204:
description: A successful response.
default:
description: Generic error response.
schema:
$ref: "#/definitions/error"
tags:
- UserAPI
/users:
get:
@@ -1367,7 +1411,11 @@ definitions:
type: array
items:
type: string
serviceAccount:
serviceAccounts:
type: array
items:
type: string
serviceAccountRequest:
type: object
properties:
policy: