Skip to content

Commit 7ec391b

Browse files
adfostdvaldiviaAdam Staffordbexsoft
authored
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>
1 parent 8c82124 commit 7ec391b

16 files changed

+889
-55
lines changed
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
// This file is part of MinIO Console Server
2+
// Copyright (c) 2021 MinIO, Inc.
3+
//
4+
// This program is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Affero General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// This program is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Affero General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Affero General Public License
15+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
17+
import React, { useEffect, useState } from "react";
18+
import { connect } from "react-redux";
19+
import Grid from "@material-ui/core/Grid";
20+
import { Button, LinearProgress } from "@material-ui/core";
21+
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
22+
import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
23+
import { NewServiceAccount } from "../Common/CredentialsPrompt/types";
24+
import { setModalErrorSnackMessage } from "../../../actions";
25+
import { ErrorResponseHandler } from "../../../common/types";
26+
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
27+
import api from "../../../common/api";
28+
import CodeMirrorWrapper from "../Common/FormComponents/CodeMirrorWrapper/CodeMirrorWrapper";
29+
import FormSwitchWrapper from "../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
30+
31+
const styles = (theme: Theme) =>
32+
createStyles({
33+
jsonPolicyEditor: {
34+
minHeight: 400,
35+
width: "100%",
36+
},
37+
buttonContainer: {
38+
textAlign: "right",
39+
},
40+
infoDetails: {
41+
color: "#393939",
42+
fontSize: 12,
43+
fontStyle: "italic",
44+
marginBottom: "8px",
45+
},
46+
containerScrollable: {
47+
maxHeight: "calc(100vh - 300px)" as const,
48+
overflowY: "auto" as const,
49+
},
50+
...modalBasic,
51+
});
52+
53+
interface IAddUserServiceAccountProps {
54+
classes: any;
55+
open: boolean;
56+
user: string;
57+
closeModalAndRefresh: (res: NewServiceAccount | null) => void;
58+
setModalErrorSnackMessage: typeof setModalErrorSnackMessage;
59+
}
60+
61+
const AddUserServiceAccount = ({
62+
classes,
63+
open,
64+
closeModalAndRefresh,
65+
setModalErrorSnackMessage,
66+
user,
67+
}: IAddUserServiceAccountProps) => {
68+
const [addSending, setAddSending] = useState<boolean>(false);
69+
const [policyDefinition, setPolicyDefinition] = useState<string>("");
70+
const [isRestrictedByPolicy, setIsRestrictedByPolicy] =
71+
useState<boolean>(false);
72+
73+
useEffect(() => {
74+
if (addSending) {
75+
api
76+
.invoke("POST", `/api/v1/user/${user}/service-accounts`, {
77+
policy: policyDefinition,
78+
})
79+
.then((res) => {
80+
setAddSending(false);
81+
closeModalAndRefresh(res);
82+
})
83+
.catch((err: ErrorResponseHandler) => {
84+
setAddSending(false);
85+
setModalErrorSnackMessage(err);
86+
});
87+
}
88+
}, [
89+
addSending,
90+
setAddSending,
91+
setModalErrorSnackMessage,
92+
policyDefinition,
93+
closeModalAndRefresh,
94+
user,
95+
]);
96+
97+
const addUserServiceAccount = (e: React.FormEvent) => {
98+
e.preventDefault();
99+
setAddSending(true);
100+
};
101+
102+
const resetForm = () => {
103+
setPolicyDefinition("");
104+
};
105+
106+
return (
107+
<ModalWrapper
108+
modalOpen={open}
109+
onClose={() => {
110+
closeModalAndRefresh(null);
111+
}}
112+
title={`Create Service Account`}
113+
>
114+
<form
115+
noValidate
116+
autoComplete="off"
117+
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
118+
addUserServiceAccount(e);
119+
}}
120+
>
121+
<Grid container className={classes.containerScrollable}>
122+
<Grid item xs={12}>
123+
<div className={classes.infoDetails}>
124+
Service Accounts inherit the policy explicitly attached to the
125+
parent user and the policy attached to each group in which the
126+
parent user has membership. You can specify an optional
127+
JSON-formatted policy below to restrict the Service Account access
128+
to a subset of actions and resources explicitly allowed for the
129+
parent user. You cannot modify the Service Account optional policy
130+
after saving.
131+
</div>
132+
</Grid>
133+
<Grid item xs={12}>
134+
<FormSwitchWrapper
135+
value="locking"
136+
id="locking"
137+
name="locking"
138+
checked={isRestrictedByPolicy}
139+
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
140+
setIsRestrictedByPolicy(event.target.checked);
141+
}}
142+
label={"Restrict with policy"}
143+
indicatorLabels={["On", "Off"]}
144+
/>
145+
</Grid>
146+
{isRestrictedByPolicy && (
147+
<Grid item xs={12}>
148+
<CodeMirrorWrapper
149+
value={policyDefinition}
150+
onBeforeChange={(editor, data, value) => {
151+
setPolicyDefinition(value);
152+
}}
153+
/>
154+
</Grid>
155+
)}
156+
</Grid>
157+
<Grid container>
158+
<Grid item xs={12} className={classes.buttonContainer}>
159+
<button
160+
type="button"
161+
color="primary"
162+
className={classes.clearButton}
163+
onClick={resetForm}
164+
>
165+
Clear
166+
</button>
167+
<Button
168+
type="submit"
169+
variant="contained"
170+
color="primary"
171+
disabled={addSending}
172+
>
173+
Create
174+
</Button>
175+
</Grid>
176+
{addSending && (
177+
<Grid item xs={12}>
178+
<LinearProgress />
179+
</Grid>
180+
)}
181+
</Grid>
182+
</form>
183+
</ModalWrapper>
184+
);
185+
};
186+
187+
const mapDispatchToProps = {
188+
setModalErrorSnackMessage,
189+
};
190+
191+
const connector = connect(null, mapDispatchToProps);
192+
193+
export default withStyles(styles)(connector(AddUserServiceAccount));

portal-ui/src/screens/Console/Users/UserDetails.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -377,11 +377,7 @@ const UserDetails = ({ classes, match }: IUserDetailsProps) => {
377377
/>
378378
</TabPanel>
379379
<TabPanel index={1} value={curTab}>
380-
<div className={classes.actionsTray}>
381-
<h1 className={classes.sectionTitle}>Service Accounts</h1>
382-
</div>
383-
<br />
384-
<UserServiceAccountsPanel user={userName} />
380+
<UserServiceAccountsPanel user={userName} classes={classes} />
385381
</TabPanel>
386382
<TabPanel index={2} value={curTab}>
387383
<div className={classes.actionsTray}>

portal-ui/src/screens/Console/Users/UserServiceAccountsPanel.tsx

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@ import { setErrorSnackMessage } from "../../../actions";
2929
import { NewServiceAccount } from "../Common/CredentialsPrompt/types";
3030
import { stringSort } from "../../../utils/sortFunctions";
3131
import { ErrorResponseHandler } from "../../../common/types";
32-
import AddServiceAccount from "../Account/AddServiceAccount";
32+
import AddUserServiceAccount from "./AddUserServiceAccount";
3333
import DeleteServiceAccount from "../Account/DeleteServiceAccount";
3434
import CredentialsPrompt from "../Common/CredentialsPrompt/CredentialsPrompt";
35+
import {CreateIcon} from "../../../icons";
36+
import Button from "@material-ui/core/Button";
3537

3638
interface IUserServiceAccountsProps {
3739
classes: any;
@@ -45,7 +47,6 @@ const styles = (theme: Theme) =>
4547
...actionsTray,
4648
actionsTray: {
4749
...actionsTray.actionsTray,
48-
padding: "15px 0 0",
4950
},
5051
});
5152

@@ -72,7 +73,7 @@ const UserServiceAccountsPanel = ({
7273
useEffect(() => {
7374
if (loading) {
7475
api
75-
.invoke("GET", `/api/v1/user/service-accounts?name=${user}`)
76+
.invoke("GET", `/api/v1/user/${user}/service-accounts`)
7677
.then((res: string[]) => {
7778
const serviceAccounts = res.sort(stringSort);
7879

@@ -131,11 +132,12 @@ const UserServiceAccountsPanel = ({
131132
return (
132133
<React.Fragment>
133134
{addScreenOpen && (
134-
<AddServiceAccount
135+
<AddUserServiceAccount
135136
open={addScreenOpen}
136137
closeModalAndRefresh={(res: NewServiceAccount | null) => {
137138
closeAddModalAndRefresh(res);
138139
}}
140+
user={user}
139141
/>
140142
)}
141143
{deleteOpen && (
@@ -157,18 +159,30 @@ const UserServiceAccountsPanel = ({
157159
entity="Service Account"
158160
/>
159161
)}
160-
<Grid container className={classes.container}>
161-
<Grid item xs={12}>
162-
<TableWrapper
163-
isLoading={loading}
164-
records={records}
165-
entityName={"Service Accounts"}
166-
idField={""}
167-
columns={[{ label: "Service Account", elementKey: "" }]}
168-
itemActions={tableActions}
169-
/>
170-
</Grid>
171-
</Grid>
162+
<div className={classes.actionsTray}>
163+
<h1 className={classes.sectionTitle}>Service Accounts</h1>
164+
<Button
165+
variant="contained"
166+
color="primary"
167+
startIcon={<CreateIcon />}
168+
onClick={() => {
169+
setAddScreenOpen(true);
170+
setAddScreenOpen(true);
171+
setSelectedServiceAccount(null);
172+
}}
173+
>
174+
Create service account
175+
</Button>
176+
</div>
177+
<br/>
178+
<TableWrapper
179+
isLoading={loading}
180+
records={records}
181+
entityName={"Service Accounts"}
182+
idField={""}
183+
columns={[{ label: "Service Account", elementKey: "" }]}
184+
itemActions={tableActions}
185+
/>
172186
</React.Fragment>
173187
);
174188
};

restapi/client-admin.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ type MinioAdmin interface {
9595
forceStart, forceStop bool) (healStart madmin.HealStartSuccess, healTaskStatus madmin.HealTaskStatus, err error)
9696
// Service Accounts
9797
addServiceAccount(ctx context.Context, policy *iampolicy.Policy) (madmin.Credentials, error)
98+
addServiceAccountWithUser(ctx context.Context, policy *iampolicy.Policy, user string) (madmin.Credentials, error)
9899
listServiceAccounts(ctx context.Context, user string) (madmin.ListServiceAccountsResp, error)
99100
deleteServiceAccount(ctx context.Context, serviceAccount string) error
100101
// Remote Buckets
@@ -286,6 +287,19 @@ func (ac AdminClient) addServiceAccount(ctx context.Context, policy *iampolicy.P
286287
})
287288
}
288289

290+
func (ac AdminClient) addServiceAccountWithUser(ctx context.Context, policy *iampolicy.Policy, user string) (madmin.Credentials, error) {
291+
buf, err := json.Marshal(policy)
292+
if err != nil {
293+
return madmin.Credentials{}, err
294+
}
295+
return ac.Client.AddServiceAccount(ctx, madmin.AddServiceAccountReq{
296+
Policy: buf,
297+
TargetUser: user,
298+
AccessKey: "",
299+
SecretKey: "",
300+
})
301+
}
302+
289303
// implements madmin.ListServiceAccounts()
290304
func (ac AdminClient) listServiceAccounts(ctx context.Context, user string) (madmin.ListServiceAccountsResp, error) {
291305
// TODO: Fix this

0 commit comments

Comments
 (0)