Skip to content

Commit

Permalink
Add user service account (#966)
Browse files Browse the repository at this point in the history
* add user service account

* changing API

Co-authored-by: Daniel Valdivia <[email protected]>
Co-authored-by: Adam Stafford <[email protected]>
Co-authored-by: Alex <[email protected]>
  • Loading branch information
4 people authored Aug 24, 2021
1 parent 8c82124 commit 7ec391b
Show file tree
Hide file tree
Showing 16 changed files with 889 additions and 55 deletions.
193 changes: 193 additions & 0 deletions portal-ui/src/screens/Console/Users/AddUserServiceAccount.tsx
Original file line number Diff line number Diff line change
@@ -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));
6 changes: 1 addition & 5 deletions portal-ui/src/screens/Console/Users/UserDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}>
Expand Down
46 changes: 30 additions & 16 deletions portal-ui/src/screens/Console/Users/UserServiceAccountsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -45,7 +47,6 @@ const styles = (theme: Theme) =>
...actionsTray,
actionsTray: {
...actionsTray.actionsTray,
padding: "15px 0 0",
},
});

Expand All @@ -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);

Expand Down Expand Up @@ -131,11 +132,12 @@ const UserServiceAccountsPanel = ({
return (
<React.Fragment>
{addScreenOpen && (
<AddServiceAccount
<AddUserServiceAccount
open={addScreenOpen}
closeModalAndRefresh={(res: NewServiceAccount | null) => {
closeAddModalAndRefresh(res);
}}
user={user}
/>
)}
{deleteOpen && (
Expand All @@ -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>
);
};
Expand Down
14 changes: 14 additions & 0 deletions restapi/client-admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 7ec391b

Please sign in to comment.