Add user service account (#966)

* add user service account

* changing API

Co-authored-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
Co-authored-by: Adam Stafford <adamstafford@Adams-MacBook-Pro.local>
Co-authored-by: Alex <33497058+bexsoft@users.noreply.github.com>
This commit is contained in:
adfost
2021-08-23 21:36:55 -07:00
committed by GitHub
parent 8c82124a57
commit 7ec391b6e0
16 changed files with 889 additions and 55 deletions

View File

@@ -0,0 +1,193 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useEffect, useState } from "react";
import { connect } from "react-redux";
import Grid from "@material-ui/core/Grid";
import { Button, LinearProgress } from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
import { NewServiceAccount } from "../Common/CredentialsPrompt/types";
import { setModalErrorSnackMessage } from "../../../actions";
import { ErrorResponseHandler } from "../../../common/types";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
import api from "../../../common/api";
import CodeMirrorWrapper from "../Common/FormComponents/CodeMirrorWrapper/CodeMirrorWrapper";
import FormSwitchWrapper from "../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
const styles = (theme: Theme) =>
createStyles({
jsonPolicyEditor: {
minHeight: 400,
width: "100%",
},
buttonContainer: {
textAlign: "right",
},
infoDetails: {
color: "#393939",
fontSize: 12,
fontStyle: "italic",
marginBottom: "8px",
},
containerScrollable: {
maxHeight: "calc(100vh - 300px)" as const,
overflowY: "auto" as const,
},
...modalBasic,
});
interface IAddUserServiceAccountProps {
classes: any;
open: boolean;
user: string;
closeModalAndRefresh: (res: NewServiceAccount | null) => void;
setModalErrorSnackMessage: typeof setModalErrorSnackMessage;
}
const AddUserServiceAccount = ({
classes,
open,
closeModalAndRefresh,
setModalErrorSnackMessage,
user,
}: IAddUserServiceAccountProps) => {
const [addSending, setAddSending] = useState<boolean>(false);
const [policyDefinition, setPolicyDefinition] = useState<string>("");
const [isRestrictedByPolicy, setIsRestrictedByPolicy] =
useState<boolean>(false);
useEffect(() => {
if (addSending) {
api
.invoke("POST", `/api/v1/user/${user}/service-accounts`, {
policy: policyDefinition,
})
.then((res) => {
setAddSending(false);
closeModalAndRefresh(res);
})
.catch((err: ErrorResponseHandler) => {
setAddSending(false);
setModalErrorSnackMessage(err);
});
}
}, [
addSending,
setAddSending,
setModalErrorSnackMessage,
policyDefinition,
closeModalAndRefresh,
user,
]);
const addUserServiceAccount = (e: React.FormEvent) => {
e.preventDefault();
setAddSending(true);
};
const resetForm = () => {
setPolicyDefinition("");
};
return (
<ModalWrapper
modalOpen={open}
onClose={() => {
closeModalAndRefresh(null);
}}
title={`Create Service Account`}
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
addUserServiceAccount(e);
}}
>
<Grid container className={classes.containerScrollable}>
<Grid item xs={12}>
<div className={classes.infoDetails}>
Service Accounts inherit the policy explicitly attached to the
parent user and the policy attached to each group in which the
parent user has membership. You can specify an optional
JSON-formatted policy below to restrict the Service Account access
to a subset of actions and resources explicitly allowed for the
parent user. You cannot modify the Service Account optional policy
after saving.
</div>
</Grid>
<Grid item xs={12}>
<FormSwitchWrapper
value="locking"
id="locking"
name="locking"
checked={isRestrictedByPolicy}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setIsRestrictedByPolicy(event.target.checked);
}}
label={"Restrict with policy"}
indicatorLabels={["On", "Off"]}
/>
</Grid>
{isRestrictedByPolicy && (
<Grid item xs={12}>
<CodeMirrorWrapper
value={policyDefinition}
onBeforeChange={(editor, data, value) => {
setPolicyDefinition(value);
}}
/>
</Grid>
)}
</Grid>
<Grid container>
<Grid item xs={12} className={classes.buttonContainer}>
<button
type="button"
color="primary"
className={classes.clearButton}
onClick={resetForm}
>
Clear
</button>
<Button
type="submit"
variant="contained"
color="primary"
disabled={addSending}
>
Create
</Button>
</Grid>
{addSending && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid>
</form>
</ModalWrapper>
);
};
const mapDispatchToProps = {
setModalErrorSnackMessage,
};
const connector = connect(null, mapDispatchToProps);
export default withStyles(styles)(connector(AddUserServiceAccount));

View File

@@ -377,11 +377,7 @@ const UserDetails = ({ classes, match }: IUserDetailsProps) => {
/>
</TabPanel>
<TabPanel index={1} value={curTab}>
<div className={classes.actionsTray}>
<h1 className={classes.sectionTitle}>Service Accounts</h1>
</div>
<br />
<UserServiceAccountsPanel user={userName} />
<UserServiceAccountsPanel user={userName} classes={classes} />
</TabPanel>
<TabPanel index={2} value={curTab}>
<div className={classes.actionsTray}>

View File

@@ -29,9 +29,11 @@ import { setErrorSnackMessage } from "../../../actions";
import { NewServiceAccount } from "../Common/CredentialsPrompt/types";
import { stringSort } from "../../../utils/sortFunctions";
import { ErrorResponseHandler } from "../../../common/types";
import AddServiceAccount from "../Account/AddServiceAccount";
import AddUserServiceAccount from "./AddUserServiceAccount";
import DeleteServiceAccount from "../Account/DeleteServiceAccount";
import CredentialsPrompt from "../Common/CredentialsPrompt/CredentialsPrompt";
import {CreateIcon} from "../../../icons";
import Button from "@material-ui/core/Button";
interface IUserServiceAccountsProps {
classes: any;
@@ -45,7 +47,6 @@ const styles = (theme: Theme) =>
...actionsTray,
actionsTray: {
...actionsTray.actionsTray,
padding: "15px 0 0",
},
});
@@ -72,7 +73,7 @@ const UserServiceAccountsPanel = ({
useEffect(() => {
if (loading) {
api
.invoke("GET", `/api/v1/user/service-accounts?name=${user}`)
.invoke("GET", `/api/v1/user/${user}/service-accounts`)
.then((res: string[]) => {
const serviceAccounts = res.sort(stringSort);
@@ -131,11 +132,12 @@ const UserServiceAccountsPanel = ({
return (
<React.Fragment>
{addScreenOpen && (
<AddServiceAccount
<AddUserServiceAccount
open={addScreenOpen}
closeModalAndRefresh={(res: NewServiceAccount | null) => {
closeAddModalAndRefresh(res);
}}
user={user}
/>
)}
{deleteOpen && (
@@ -157,18 +159,30 @@ const UserServiceAccountsPanel = ({
entity="Service Account"
/>
)}
<Grid container className={classes.container}>
<Grid item xs={12}>
<TableWrapper
isLoading={loading}
records={records}
entityName={"Service Accounts"}
idField={""}
columns={[{ label: "Service Account", elementKey: "" }]}
itemActions={tableActions}
/>
</Grid>
</Grid>
<div className={classes.actionsTray}>
<h1 className={classes.sectionTitle}>Service Accounts</h1>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddScreenOpen(true);
setAddScreenOpen(true);
setSelectedServiceAccount(null);
}}
>
Create service account
</Button>
</div>
<br/>
<TableWrapper
isLoading={loading}
records={records}
entityName={"Service Accounts"}
idField={""}
columns={[{ label: "Service Account", elementKey: "" }]}
itemActions={tableActions}
/>
</React.Fragment>
);
};

View File

@@ -95,6 +95,7 @@ type MinioAdmin interface {
forceStart, forceStop bool) (healStart madmin.HealStartSuccess, healTaskStatus madmin.HealTaskStatus, err error)
// Service Accounts
addServiceAccount(ctx context.Context, policy *iampolicy.Policy) (madmin.Credentials, error)
addServiceAccountWithUser(ctx context.Context, policy *iampolicy.Policy, user string) (madmin.Credentials, error)
listServiceAccounts(ctx context.Context, user string) (madmin.ListServiceAccountsResp, error)
deleteServiceAccount(ctx context.Context, serviceAccount string) error
// Remote Buckets
@@ -286,6 +287,19 @@ func (ac AdminClient) addServiceAccount(ctx context.Context, policy *iampolicy.P
})
}
func (ac AdminClient) addServiceAccountWithUser(ctx context.Context, policy *iampolicy.Policy, user string) (madmin.Credentials, error) {
buf, err := json.Marshal(policy)
if err != nil {
return madmin.Credentials{}, err
}
return ac.Client.AddServiceAccount(ctx, madmin.AddServiceAccountReq{
Policy: buf,
TargetUser: user,
AccessKey: "",
SecretKey: "",
})
}
// implements madmin.ListServiceAccounts()
func (ac AdminClient) listServiceAccounts(ctx context.Context, user string) (madmin.ListServiceAccountsResp, error) {
// TODO: Fix this

View File

@@ -3180,7 +3180,7 @@ func init() {
}
}
},
"/user/service-accounts": {
"/user/{name}/service-accounts": {
"get": {
"tags": [
"AdminAPI"
@@ -3191,7 +3191,7 @@ func init() {
{
"type": "string",
"name": "name",
"in": "query",
"in": "path",
"required": true
}
],
@@ -3209,6 +3209,43 @@ func init() {
}
}
}
},
"post": {
"tags": [
"AdminAPI"
],
"summary": "Create Service Account for User",
"operationId": "CreateAUserServiceAccount",
"parameters": [
{
"type": "string",
"name": "name",
"in": "path",
"required": true
},
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/serviceAccountRequest"
}
}
],
"responses": {
"201": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/serviceAccountCreds"
}
},
"default": {
"description": "Generic error response.",
"schema": {
"$ref": "#/definitions/error"
}
}
}
}
},
"/users": {
@@ -8507,7 +8544,7 @@ func init() {
}
}
},
"/user/service-accounts": {
"/user/{name}/service-accounts": {
"get": {
"tags": [
"AdminAPI"
@@ -8518,7 +8555,7 @@ func init() {
{
"type": "string",
"name": "name",
"in": "query",
"in": "path",
"required": true
}
],
@@ -8536,6 +8573,43 @@ func init() {
}
}
}
},
"post": {
"tags": [
"AdminAPI"
],
"summary": "Create Service Account for User",
"operationId": "CreateAUserServiceAccount",
"parameters": [
{
"type": "string",
"name": "name",
"in": "path",
"required": true
},
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/serviceAccountRequest"
}
}
],
"responses": {
"201": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/serviceAccountCreds"
}
},
"default": {
"description": "Generic error response.",
"schema": {
"$ref": "#/definitions/error"
}
}
}
}
},
"/users": {

View File

@@ -0,0 +1,88 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2021 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package admin_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/console/models"
)
// CreateAUserServiceAccountHandlerFunc turns a function with the right signature into a create a user service account handler
type CreateAUserServiceAccountHandlerFunc func(CreateAUserServiceAccountParams, *models.Principal) middleware.Responder
// Handle executing the request and returning a response
func (fn CreateAUserServiceAccountHandlerFunc) Handle(params CreateAUserServiceAccountParams, principal *models.Principal) middleware.Responder {
return fn(params, principal)
}
// CreateAUserServiceAccountHandler interface for that can handle valid create a user service account params
type CreateAUserServiceAccountHandler interface {
Handle(CreateAUserServiceAccountParams, *models.Principal) middleware.Responder
}
// NewCreateAUserServiceAccount creates a new http.Handler for the create a user service account operation
func NewCreateAUserServiceAccount(ctx *middleware.Context, handler CreateAUserServiceAccountHandler) *CreateAUserServiceAccount {
return &CreateAUserServiceAccount{Context: ctx, Handler: handler}
}
/* CreateAUserServiceAccount swagger:route POST /user/{name}/service-accounts AdminAPI createAUserServiceAccount
Create Service Account for User
*/
type CreateAUserServiceAccount struct {
Context *middleware.Context
Handler CreateAUserServiceAccountHandler
}
func (o *CreateAUserServiceAccount) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
route, rCtx, _ := o.Context.RouteInfo(r)
if rCtx != nil {
*r = *rCtx
}
var Params = NewCreateAUserServiceAccountParams()
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,127 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2021 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package admin_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"
"io"
"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/validate"
"github.com/minio/console/models"
)
// NewCreateAUserServiceAccountParams creates a new CreateAUserServiceAccountParams object
//
// There are no default values defined in the spec.
func NewCreateAUserServiceAccountParams() CreateAUserServiceAccountParams {
return CreateAUserServiceAccountParams{}
}
// CreateAUserServiceAccountParams contains all the bound params for the create a user service account operation
// typically these are obtained from a http.Request
//
// swagger:parameters CreateAUserServiceAccount
type CreateAUserServiceAccountParams struct {
// HTTP Request Object
HTTPRequest *http.Request `json:"-"`
/*
Required: true
In: body
*/
Body *models.ServiceAccountRequest
/*
Required: true
In: path
*/
Name 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 NewCreateAUserServiceAccountParams() beforehand.
func (o *CreateAUserServiceAccountParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {
var res []error
o.HTTPRequest = r
if runtime.HasBody(r) {
defer r.Body.Close()
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", ""))
} else {
res = append(res, errors.NewParseError("body", "body", "", err))
}
} else {
// validate body object
if err := body.Validate(route.Formats); err != nil {
res = append(res, err)
}
ctx := validate.WithOperationRequest(context.Background())
if err := body.ContextValidate(ctx, route.Formats); err != nil {
res = append(res, err)
}
if len(res) == 0 {
o.Body = &body
}
}
} else {
res = append(res, errors.Required("body", "body", ""))
}
rName, rhkName, _ := route.Params.GetOK("name")
if err := o.bindName(rName, rhkName, route.Formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
// bindName binds and validates parameter Name from path.
func (o *CreateAUserServiceAccountParams) bindName(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.Name = raw
return nil
}

View File

@@ -0,0 +1,133 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2021 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package admin_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/console/models"
)
// CreateAUserServiceAccountCreatedCode is the HTTP code returned for type CreateAUserServiceAccountCreated
const CreateAUserServiceAccountCreatedCode int = 201
/*CreateAUserServiceAccountCreated A successful response.
swagger:response createAUserServiceAccountCreated
*/
type CreateAUserServiceAccountCreated struct {
/*
In: Body
*/
Payload *models.ServiceAccountCreds `json:"body,omitempty"`
}
// NewCreateAUserServiceAccountCreated creates CreateAUserServiceAccountCreated with default headers values
func NewCreateAUserServiceAccountCreated() *CreateAUserServiceAccountCreated {
return &CreateAUserServiceAccountCreated{}
}
// WithPayload adds the payload to the create a user service account created response
func (o *CreateAUserServiceAccountCreated) WithPayload(payload *models.ServiceAccountCreds) *CreateAUserServiceAccountCreated {
o.Payload = payload
return o
}
// SetPayload sets the payload to the create a user service account created response
func (o *CreateAUserServiceAccountCreated) SetPayload(payload *models.ServiceAccountCreds) {
o.Payload = payload
}
// WriteResponse to the client
func (o *CreateAUserServiceAccountCreated) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.WriteHeader(201)
if o.Payload != nil {
payload := o.Payload
if err := producer.Produce(rw, payload); err != nil {
panic(err) // let the recovery middleware deal with this
}
}
}
/*CreateAUserServiceAccountDefault Generic error response.
swagger:response createAUserServiceAccountDefault
*/
type CreateAUserServiceAccountDefault struct {
_statusCode int
/*
In: Body
*/
Payload *models.Error `json:"body,omitempty"`
}
// NewCreateAUserServiceAccountDefault creates CreateAUserServiceAccountDefault with default headers values
func NewCreateAUserServiceAccountDefault(code int) *CreateAUserServiceAccountDefault {
if code <= 0 {
code = 500
}
return &CreateAUserServiceAccountDefault{
_statusCode: code,
}
}
// WithStatusCode adds the status to the create a user service account default response
func (o *CreateAUserServiceAccountDefault) WithStatusCode(code int) *CreateAUserServiceAccountDefault {
o._statusCode = code
return o
}
// SetStatusCode sets the status to the create a user service account default response
func (o *CreateAUserServiceAccountDefault) SetStatusCode(code int) {
o._statusCode = code
}
// WithPayload adds the payload to the create a user service account default response
func (o *CreateAUserServiceAccountDefault) WithPayload(payload *models.Error) *CreateAUserServiceAccountDefault {
o.Payload = payload
return o
}
// SetPayload sets the payload to the create a user service account default response
func (o *CreateAUserServiceAccountDefault) SetPayload(payload *models.Error) {
o.Payload = payload
}
// WriteResponse to the client
func (o *CreateAUserServiceAccountDefault) 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) 2021 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package admin_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"
)
// CreateAUserServiceAccountURL generates an URL for the create a user service account operation
type CreateAUserServiceAccountURL struct {
Name 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 *CreateAUserServiceAccountURL) WithBasePath(bp string) *CreateAUserServiceAccountURL {
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 *CreateAUserServiceAccountURL) SetBasePath(bp string) {
o._basePath = bp
}
// Build a url path and query string
func (o *CreateAUserServiceAccountURL) Build() (*url.URL, error) {
var _result url.URL
var _path = "/user/{name}/service-accounts"
name := o.Name
if name != "" {
_path = strings.Replace(_path, "{name}", name, -1)
} else {
return nil, errors.New("name is required on CreateAUserServiceAccountURL")
}
_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 *CreateAUserServiceAccountURL) 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 *CreateAUserServiceAccountURL) String() string {
return o.Must(o.Build()).String()
}
// BuildFull builds a full url with scheme, host, path and query string
func (o *CreateAUserServiceAccountURL) BuildFull(scheme, host string) (*url.URL, error) {
if scheme == "" {
return nil, errors.New("scheme is required for a full url on CreateAUserServiceAccountURL")
}
if host == "" {
return nil, errors.New("host is required for a full url on CreateAUserServiceAccountURL")
}
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 *CreateAUserServiceAccountURL) StringFull(scheme, host string) string {
return o.Must(o.BuildFull(scheme, host)).String()
}

View File

@@ -48,7 +48,7 @@ func NewListAUserServiceAccounts(ctx *middleware.Context, handler ListAUserServi
return &ListAUserServiceAccounts{Context: ctx, Handler: handler}
}
/* ListAUserServiceAccounts swagger:route GET /user/service-accounts AdminAPI listAUserServiceAccounts
/* ListAUserServiceAccounts swagger:route GET /user/{name}/service-accounts AdminAPI listAUserServiceAccounts
returns a list of service accounts for a user

View File

@@ -26,10 +26,8 @@ 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/validate"
)
// NewListAUserServiceAccountsParams creates a new ListAUserServiceAccountsParams object
@@ -51,7 +49,7 @@ type ListAUserServiceAccountsParams struct {
/*
Required: true
In: query
In: path
*/
Name string
}
@@ -65,10 +63,8 @@ func (o *ListAUserServiceAccountsParams) BindRequest(r *http.Request, route *mid
o.HTTPRequest = r
qs := runtime.Values(r.URL.Query())
qName, qhkName, _ := qs.GetOK("name")
if err := o.bindName(qName, qhkName, route.Formats); err != nil {
rName, rhkName, _ := route.Params.GetOK("name")
if err := o.bindName(rName, rhkName, route.Formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
@@ -77,22 +73,15 @@ func (o *ListAUserServiceAccountsParams) BindRequest(r *http.Request, route *mid
return nil
}
// bindName binds and validates parameter Name from query.
// bindName binds and validates parameter Name from path.
func (o *ListAUserServiceAccountsParams) bindName(rawData []string, hasKey bool, formats strfmt.Registry) error {
if !hasKey {
return errors.Required("name", "query", rawData)
}
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: true
// AllowEmptyValue: false
if err := validate.RequiredString("name", "query", raw); err != nil {
return err
}
// Parameter is provided by construction from the route
o.Name = raw
return nil

View File

@@ -26,6 +26,7 @@ import (
"errors"
"net/url"
golangswaggerpaths "path"
"strings"
)
// ListAUserServiceAccountsURL generates an URL for the list a user service accounts operation
@@ -56,7 +57,14 @@ func (o *ListAUserServiceAccountsURL) SetBasePath(bp string) {
func (o *ListAUserServiceAccountsURL) Build() (*url.URL, error) {
var _result url.URL
var _path = "/user/service-accounts"
var _path = "/user/{name}/service-accounts"
name := o.Name
if name != "" {
_path = strings.Replace(_path, "{name}", name, -1)
} else {
return nil, errors.New("name is required on ListAUserServiceAccountsURL")
}
_basePath := o._basePath
if _basePath == "" {
@@ -64,15 +72,6 @@ func (o *ListAUserServiceAccountsURL) Build() (*url.URL, error) {
}
_result.Path = golangswaggerpaths.Join(_basePath, _path)
qs := make(url.Values)
nameQ := o.Name
if nameQ != "" {
qs.Set("name", nameQ)
}
_result.RawQuery = qs.Encode()
return &_result, nil
}

View File

@@ -110,6 +110,9 @@ func NewConsoleAPI(spec *loads.Document) *ConsoleAPI {
AdminAPIConfigInfoHandler: admin_api.ConfigInfoHandlerFunc(func(params admin_api.ConfigInfoParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation admin_api.ConfigInfo has not yet been implemented")
}),
AdminAPICreateAUserServiceAccountHandler: admin_api.CreateAUserServiceAccountHandlerFunc(func(params admin_api.CreateAUserServiceAccountParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation admin_api.CreateAUserServiceAccount has not yet been implemented")
}),
UserAPICreateBucketEventHandler: user_api.CreateBucketEventHandlerFunc(func(params user_api.CreateBucketEventParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation user_api.CreateBucketEvent has not yet been implemented")
}),
@@ -435,6 +438,8 @@ type ConsoleAPI struct {
AdminAPIChangeUserPasswordHandler admin_api.ChangeUserPasswordHandler
// AdminAPIConfigInfoHandler sets the operation handler for the config info operation
AdminAPIConfigInfoHandler admin_api.ConfigInfoHandler
// AdminAPICreateAUserServiceAccountHandler sets the operation handler for the create a user service account operation
AdminAPICreateAUserServiceAccountHandler admin_api.CreateAUserServiceAccountHandler
// UserAPICreateBucketEventHandler sets the operation handler for the create bucket event operation
UserAPICreateBucketEventHandler user_api.CreateBucketEventHandler
// UserAPICreateServiceAccountHandler sets the operation handler for the create service account operation
@@ -727,6 +732,9 @@ func (o *ConsoleAPI) Validate() error {
if o.AdminAPIConfigInfoHandler == nil {
unregistered = append(unregistered, "admin_api.ConfigInfoHandler")
}
if o.AdminAPICreateAUserServiceAccountHandler == nil {
unregistered = append(unregistered, "admin_api.CreateAUserServiceAccountHandler")
}
if o.UserAPICreateBucketEventHandler == nil {
unregistered = append(unregistered, "user_api.CreateBucketEventHandler")
}
@@ -1132,6 +1140,10 @@ func (o *ConsoleAPI) initHandlerCache() {
if o.handlers["POST"] == nil {
o.handlers["POST"] = make(map[string]http.Handler)
}
o.handlers["POST"]["/user/{name}/service-accounts"] = admin_api.NewCreateAUserServiceAccount(o.context, o.AdminAPICreateAUserServiceAccountHandler)
if o.handlers["POST"] == nil {
o.handlers["POST"] = make(map[string]http.Handler)
}
o.handlers["POST"]["/buckets/{bucket_name}/events"] = user_api.NewCreateBucketEvent(o.context, o.UserAPICreateBucketEventHandler)
if o.handlers["POST"] == nil {
o.handlers["POST"] = make(map[string]http.Handler)
@@ -1244,7 +1256,7 @@ func (o *ConsoleAPI) initHandlerCache() {
if o.handlers["GET"] == nil {
o.handlers["GET"] = make(map[string]http.Handler)
}
o.handlers["GET"]["/user/service-accounts"] = admin_api.NewListAUserServiceAccounts(o.context, o.AdminAPIListAUserServiceAccountsHandler)
o.handlers["GET"]["/user/{name}/service-accounts"] = admin_api.NewListAUserServiceAccounts(o.context, o.AdminAPIListAUserServiceAccountsHandler)
if o.handlers["GET"] == nil {
o.handlers["GET"] = make(map[string]http.Handler)
}

View File

@@ -40,6 +40,14 @@ func registerServiceAccountsHandlers(api *operations.ConsoleAPI) {
}
return user_api.NewCreateServiceAccountCreated().WithPayload(creds)
})
// Create User Service Account
api.AdminAPICreateAUserServiceAccountHandler = admin_api.CreateAUserServiceAccountHandlerFunc(func(params admin_api.CreateAUserServiceAccountParams, session *models.Principal) middleware.Responder {
creds, err := getCreateAUserServiceAccountResponse(session, params.Body, params.Name)
if err != nil {
return user_api.NewCreateServiceAccountDefault(int(err.Code)).WithPayload(err)
}
return admin_api.NewCreateAUserServiceAccountCreated().WithPayload(creds)
})
// List Service Accounts for User
api.UserAPIListUserServiceAccountsHandler = user_api.ListUserServiceAccountsHandlerFunc(func(params user_api.ListUserServiceAccountsParams, session *models.Principal) middleware.Responder {
serviceAccounts, err := getUserServiceAccountsResponse(session, "")
@@ -110,6 +118,48 @@ func getCreateServiceAccountResponse(session *models.Principal, serviceAccount *
return saCreds, nil
}
// createServiceAccount adds a service account to the userClient and assigns a policy to him if defined.
func createAUserServiceAccount(ctx context.Context, userClient MinioAdmin, policy string, user string) (*models.ServiceAccountCreds, error) {
// By default a nil policy will be used so the service account inherit the parent account policy, otherwise
// we override with the user provided iam policy
var iamPolicy *iampolicy.Policy
if strings.TrimSpace(policy) != "" {
iamp, err := iampolicy.ParseConfig(bytes.NewReader([]byte(policy)))
if err != nil {
return nil, err
}
iamPolicy = iamp
}
creds, err := userClient.addServiceAccountWithUser(ctx, iamPolicy, user)
if err != nil {
return nil, err
}
return &models.ServiceAccountCreds{AccessKey: creds.AccessKey, SecretKey: creds.SecretKey}, nil
}
// 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 getCreateAUserServiceAccountResponse(session *models.Principal, serviceAccount *models.ServiceAccountRequest, user string) (*models.ServiceAccountCreds, *models.Error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
defer cancel()
userAdmin, err := NewMinioAdminClient(session)
if err != nil {
return nil, prepareError(err)
}
// create a MinIO user Admin Client interface implementation
// defining the client to be used
userAdminClient := AdminClient{Client: userAdmin}
saCreds, err := createAUserServiceAccount(ctx, userAdminClient, serviceAccount.Policy, user)
if err != nil {
return nil, prepareError(err)
}
return saCreds, nil
}
// getUserServiceAccount gets list of the user's service accounts
func getUserServiceAccounts(ctx context.Context, userClient MinioAdmin, user string) (models.ServiceAccounts, error) {
listServAccs, err := userClient.listServiceAccounts(ctx, user)

View File

@@ -30,6 +30,7 @@ import (
// assigning mock at runtime instead of compile time
var minioAddServiceAccountMock func(ctx context.Context, policy *iampolicy.Policy) (madmin.Credentials, error)
var minioAddServiceAccountWithUserMock func(ctx context.Context, policy *iampolicy.Policy, user string) (madmin.Credentials, error)
var minioListServiceAccountsMock func(ctx context.Context, user string) (madmin.ListServiceAccountsResp, error)
var minioDeleteServiceAccountMock func(ctx context.Context, serviceAccount string) error
@@ -38,6 +39,10 @@ func (ac adminClientMock) addServiceAccount(ctx context.Context, policy *iampoli
return minioAddServiceAccountMock(ctx, policy)
}
func (ac adminClientMock) addServiceAccountWithUser(ctx context.Context, policy *iampolicy.Policy, user string) (madmin.Credentials, error) {
return minioAddServiceAccountWithUserMock(ctx, policy, user)
}
// mock function of ListServiceAccounts()
func (ac adminClientMock) listServiceAccounts(ctx context.Context, user string) (madmin.ListServiceAccountsResp, error) {
return minioListServiceAccountsMock(ctx, user)

View File

@@ -1299,13 +1299,13 @@ paths:
tags:
- AdminAPI
/user/service-accounts:
/user/{name}/service-accounts:
get:
summary: returns a list of service accounts for a user
operationId: ListAUserServiceAccounts
parameters:
- name: name
in: query
in: path
required: true
type: string
responses:
@@ -1319,6 +1319,30 @@ paths:
$ref: "#/definitions/error"
tags:
- AdminAPI
post:
summary: Create Service Account for User
operationId: CreateAUserServiceAccount
parameters:
- name: name
in: path
required: true
type: string
- name: body
in: body
required: true
schema:
$ref: "#/definitions/serviceAccountRequest"
responses:
201:
description: A successful response.
schema:
$ref: "#/definitions/serviceAccountCreds"
default:
description: Generic error response.
schema:
$ref: "#/definitions/error"
tags:
- AdminAPI
/users-groups-bulk:
put: