diff --git a/portal-ui/src/screens/Console/Users/AddUserServiceAccount.tsx b/portal-ui/src/screens/Console/Users/AddUserServiceAccount.tsx new file mode 100644 index 0000000000..8ca6b6502c --- /dev/null +++ b/portal-ui/src/screens/Console/Users/AddUserServiceAccount.tsx @@ -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 . + +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(false); + const [policyDefinition, setPolicyDefinition] = useState(""); + const [isRestrictedByPolicy, setIsRestrictedByPolicy] = + useState(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 ( + { + closeModalAndRefresh(null); + }} + title={`Create Service Account`} + > +
) => { + addUserServiceAccount(e); + }} + > + + +
+ 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. +
+
+ + ) => { + setIsRestrictedByPolicy(event.target.checked); + }} + label={"Restrict with policy"} + indicatorLabels={["On", "Off"]} + /> + + {isRestrictedByPolicy && ( + + { + setPolicyDefinition(value); + }} + /> + + )} +
+ + + + + + {addSending && ( + + + + )} + +
+
+ ); +}; + +const mapDispatchToProps = { + setModalErrorSnackMessage, +}; + +const connector = connect(null, mapDispatchToProps); + +export default withStyles(styles)(connector(AddUserServiceAccount)); diff --git a/portal-ui/src/screens/Console/Users/UserDetails.tsx b/portal-ui/src/screens/Console/Users/UserDetails.tsx index 1aad2b8e5f..74cd053acc 100644 --- a/portal-ui/src/screens/Console/Users/UserDetails.tsx +++ b/portal-ui/src/screens/Console/Users/UserDetails.tsx @@ -377,11 +377,7 @@ const UserDetails = ({ classes, match }: IUserDetailsProps) => { /> -
-

Service Accounts

-
-
- +
diff --git a/portal-ui/src/screens/Console/Users/UserServiceAccountsPanel.tsx b/portal-ui/src/screens/Console/Users/UserServiceAccountsPanel.tsx index d7bd6c4cea..b2a5eae0ff 100644 --- a/portal-ui/src/screens/Console/Users/UserServiceAccountsPanel.tsx +++ b/portal-ui/src/screens/Console/Users/UserServiceAccountsPanel.tsx @@ -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 ( {addScreenOpen && ( - { closeAddModalAndRefresh(res); }} + user={user} /> )} {deleteOpen && ( @@ -157,18 +159,30 @@ const UserServiceAccountsPanel = ({ entity="Service Account" /> )} - - - - - +
+

Service Accounts

+ +
+
+
); }; diff --git a/restapi/client-admin.go b/restapi/client-admin.go index 606057a90f..423d20ee88 100644 --- a/restapi/client-admin.go +++ b/restapi/client-admin.go @@ -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 diff --git a/restapi/embedded_spec.go b/restapi/embedded_spec.go index bd770a98ae..4e3ceb9cfd 100644 --- a/restapi/embedded_spec.go +++ b/restapi/embedded_spec.go @@ -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": { diff --git a/restapi/operations/admin_api/create_a_user_service_account.go b/restapi/operations/admin_api/create_a_user_service_account.go new file mode 100644 index 0000000000..1253a6be60 --- /dev/null +++ b/restapi/operations/admin_api/create_a_user_service_account.go @@ -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 . +// + +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) + +} diff --git a/restapi/operations/admin_api/create_a_user_service_account_parameters.go b/restapi/operations/admin_api/create_a_user_service_account_parameters.go new file mode 100644 index 0000000000..2240f588c6 --- /dev/null +++ b/restapi/operations/admin_api/create_a_user_service_account_parameters.go @@ -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 . +// + +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 +} diff --git a/restapi/operations/admin_api/create_a_user_service_account_responses.go b/restapi/operations/admin_api/create_a_user_service_account_responses.go new file mode 100644 index 0000000000..1776d757f7 --- /dev/null +++ b/restapi/operations/admin_api/create_a_user_service_account_responses.go @@ -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 . +// + +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 + } + } +} diff --git a/restapi/operations/admin_api/create_a_user_service_account_urlbuilder.go b/restapi/operations/admin_api/create_a_user_service_account_urlbuilder.go new file mode 100644 index 0000000000..31bb98620e --- /dev/null +++ b/restapi/operations/admin_api/create_a_user_service_account_urlbuilder.go @@ -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 . +// + +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() +} diff --git a/restapi/operations/admin_api/list_a_user_service_accounts.go b/restapi/operations/admin_api/list_a_user_service_accounts.go index cca0bf5921..5c4f186860 100644 --- a/restapi/operations/admin_api/list_a_user_service_accounts.go +++ b/restapi/operations/admin_api/list_a_user_service_accounts.go @@ -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 diff --git a/restapi/operations/admin_api/list_a_user_service_accounts_parameters.go b/restapi/operations/admin_api/list_a_user_service_accounts_parameters.go index 109e116a6f..e48afe686e 100644 --- a/restapi/operations/admin_api/list_a_user_service_accounts_parameters.go +++ b/restapi/operations/admin_api/list_a_user_service_accounts_parameters.go @@ -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 diff --git a/restapi/operations/admin_api/list_a_user_service_accounts_urlbuilder.go b/restapi/operations/admin_api/list_a_user_service_accounts_urlbuilder.go index 7ec69fadcb..825718562c 100644 --- a/restapi/operations/admin_api/list_a_user_service_accounts_urlbuilder.go +++ b/restapi/operations/admin_api/list_a_user_service_accounts_urlbuilder.go @@ -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 } diff --git a/restapi/operations/console_api.go b/restapi/operations/console_api.go index 8fbfc184c9..55ec9c1022 100644 --- a/restapi/operations/console_api.go +++ b/restapi/operations/console_api.go @@ -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) } diff --git a/restapi/user_service_accounts.go b/restapi/user_service_accounts.go index 04652906e1..e196ff019f 100644 --- a/restapi/user_service_accounts.go +++ b/restapi/user_service_accounts.go @@ -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) diff --git a/restapi/user_service_accounts_test.go b/restapi/user_service_accounts_test.go index 940d61d4c3..6509efae5d 100644 --- a/restapi/user_service_accounts_test.go +++ b/restapi/user_service_accounts_test.go @@ -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) diff --git a/swagger-console.yml b/swagger-console.yml index 94ff1cb6a5..a4b415e9d2 100644 --- a/swagger-console.yml +++ b/swagger-console.yml @@ -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: