github.com/minio/console@v1.4.1/web-app/src/screens/Console/Users/AddUserServiceAccountScreen.tsx (about) 1 // This file is part of MinIO Console Server 2 // Copyright (c) 2022 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, { Fragment, useEffect, useState } from "react"; 18 import { useNavigate, useParams } from "react-router-dom"; 19 import { 20 BackLink, 21 Box, 22 Button, 23 FormLayout, 24 Grid, 25 InputBox, 26 PageLayout, 27 PasswordKeyIcon, 28 ServiceAccountCredentialsIcon, 29 ServiceAccountIcon, 30 Switch, 31 HelpTip, 32 DateTimeInput, 33 } from "mds"; 34 import { modalStyleUtils } from "../Common/FormComponents/common/styleLibrary"; 35 import { NewServiceAccount } from "../Common/CredentialsPrompt/types"; 36 import { IAM_PAGES } from "../../../common/SecureComponent/permissions"; 37 import { ErrorResponseHandler } from "../../../common/types"; 38 import { 39 decodeURLString, 40 encodeURLString, 41 getRandomString, 42 } from "../../../common/utils"; 43 import { setErrorSnackMessage, setHelpName } from "../../../systemSlice"; 44 import { useAppDispatch } from "../../../store"; 45 import CodeMirrorWrapper from "../Common/FormComponents/CodeMirrorWrapper/CodeMirrorWrapper"; 46 import api from "../../../../src/common/api"; 47 import CredentialsPrompt from "../Common/CredentialsPrompt/CredentialsPrompt"; 48 import AddUserServiceAccountHelpBox from "./AddUserServiceAccountHelpBox"; 49 import PageHeaderWrapper from "../Common/PageHeaderWrapper/PageHeaderWrapper"; 50 import HelpMenu from "../HelpMenu"; 51 import PanelTitle from "../Common/PanelTitle/PanelTitle"; 52 53 const AddServiceAccount = () => { 54 const dispatch = useAppDispatch(); 55 const params = useParams(); 56 const navigate = useNavigate(); 57 58 const [addSending, setAddSending] = useState<boolean>(false); 59 const [accessKey, setAccessKey] = useState<string>(getRandomString(20)); 60 const [secretKey, setSecretKey] = useState<string>(getRandomString(40)); 61 const [isRestrictedByPolicy, setIsRestrictedByPolicy] = 62 useState<boolean>(false); 63 const [newServiceAccount, setNewServiceAccount] = 64 useState<NewServiceAccount | null>(null); 65 const [policyJSON, setPolicyJSON] = useState<string>(""); 66 67 const userName = decodeURLString(params.userName || ""); 68 69 const [name, setName] = useState<string>(""); 70 const [description, setDescription] = useState<string>(""); 71 const [comments, setComments] = useState<string>(""); 72 const [expiry, setExpiry] = useState<any>(); 73 74 useEffect(() => { 75 if (addSending) { 76 const expiryDt = expiry ? expiry.toJSDate().toISOString() : null; 77 api 78 .invoke( 79 "POST", 80 `/api/v1/user/${encodeURLString( 81 userName, 82 )}/service-account-credentials`, 83 { 84 policy: policyJSON, 85 accessKey: accessKey, 86 secretKey: secretKey, 87 description: description, 88 comment: comments, 89 name: name, 90 expiry: expiryDt, 91 }, 92 ) 93 .then((res) => { 94 setAddSending(false); 95 setNewServiceAccount({ 96 accessKey: res.accessKey || "", 97 secretKey: res.secretKey || "", 98 url: res.url || "", 99 }); 100 }) 101 .catch((err: ErrorResponseHandler) => { 102 setAddSending(false); 103 dispatch(setErrorSnackMessage(err)); 104 }); 105 } 106 }, [ 107 addSending, 108 setAddSending, 109 dispatch, 110 policyJSON, 111 userName, 112 accessKey, 113 secretKey, 114 name, 115 description, 116 expiry, 117 comments, 118 ]); 119 120 useEffect(() => { 121 if (isRestrictedByPolicy) { 122 api 123 .invoke("GET", `/api/v1/user/${encodeURLString(userName)}/policies`) 124 125 .then((res) => { 126 setPolicyJSON(JSON.stringify(JSON.parse(res.policy), null, 4)); 127 }) 128 .catch((err: ErrorResponseHandler) => { 129 setErrorSnackMessage(err); 130 }); 131 } 132 }, [isRestrictedByPolicy, userName]); 133 134 const addUserServiceAccount = (e: React.FormEvent) => { 135 e.preventDefault(); 136 setAddSending(true); 137 }; 138 139 const resetForm = () => { 140 setNewServiceAccount(null); 141 setAccessKey(""); 142 setSecretKey(""); 143 }; 144 145 const closeCredentialsModal = () => { 146 setNewServiceAccount(null); 147 navigate(`${IAM_PAGES.USERS}/${encodeURLString(userName)}`); 148 }; 149 150 useEffect(() => { 151 dispatch(setHelpName("add_user_SA")); 152 // eslint-disable-next-line react-hooks/exhaustive-deps 153 }, []); 154 155 return ( 156 <Fragment> 157 {newServiceAccount && ( 158 <CredentialsPrompt 159 newServiceAccount={newServiceAccount} 160 open 161 closeModal={() => { 162 closeCredentialsModal(); 163 }} 164 entity="Access Key" 165 /> 166 )} 167 <Grid item xs={12}> 168 <PageHeaderWrapper 169 label={ 170 <BackLink 171 onClick={() => 172 navigate(`${IAM_PAGES.USERS}/${encodeURLString(userName)}`) 173 } 174 label={"User Details - " + userName} 175 /> 176 } 177 actions={<HelpMenu />} 178 /> 179 <PageLayout> 180 <FormLayout 181 helpBox={<AddUserServiceAccountHelpBox />} 182 icon={<ServiceAccountCredentialsIcon />} 183 title={`Create Access Key for ${userName}`} 184 > 185 <form 186 noValidate 187 autoComplete="off" 188 onSubmit={(e: React.FormEvent<HTMLFormElement>) => { 189 e.preventDefault(); 190 addUserServiceAccount(e); 191 }} 192 > 193 <InputBox 194 value={accessKey} 195 label={"Access Key"} 196 id={"accessKey"} 197 name={"accessKey"} 198 placeholder={"Enter Access Key"} 199 onChange={(e) => { 200 setAccessKey(e.target.value); 201 }} 202 startIcon={<ServiceAccountIcon />} 203 /> 204 <InputBox 205 value={secretKey} 206 label={"Secret Key"} 207 id={"secretKey"} 208 name={"secretKey"} 209 type={"password"} 210 placeholder={"Enter Secret Key"} 211 onChange={(e) => { 212 setSecretKey(e.target.value); 213 }} 214 startIcon={<PasswordKeyIcon />} 215 /> 216 217 <Switch 218 value="serviceAccountPolicy" 219 id="serviceAccountPolicy" 220 name="serviceAccountPolicy" 221 checked={isRestrictedByPolicy} 222 onChange={(event: React.ChangeEvent<HTMLInputElement>) => { 223 setIsRestrictedByPolicy(event.target.checked); 224 }} 225 label={"Restrict beyond user policy"} 226 description={ 227 "You can specify an optional JSON-formatted IAM policy to further restrict Access Key access to a subset of the actions and resources explicitly allowed for the parent user. Additional access beyond that of the parent user cannot be implemented through these policies." 228 } 229 /> 230 {isRestrictedByPolicy && ( 231 <Grid item xs={12}> 232 <Box> 233 <HelpTip 234 content={ 235 <Fragment> 236 <a 237 target="blank" 238 href="https://min.io/docs/minio/kubernetes/upstream/administration/identity-access-management/policy-based-access-control.html#policy-document-structure" 239 > 240 Guide to access policy structure 241 </a> 242 </Fragment> 243 } 244 placement="right" 245 > 246 <PanelTitle> 247 Current User Policy - edit the JSON to remove 248 permissions for this Access Key 249 </PanelTitle> 250 </HelpTip> 251 </Box> 252 <Grid item xs={12} sx={{ ...modalStyleUtils.formScrollable }}> 253 <CodeMirrorWrapper 254 value={policyJSON} 255 onChange={(value) => { 256 setPolicyJSON(value); 257 }} 258 editorHeight={"350px"} 259 helptip={ 260 <Fragment> 261 <a 262 target="blank" 263 href="https://min.io/docs/minio/kubernetes/upstream/administration/identity-access-management/policy-based-access-control.html#policy-document-structure" 264 > 265 Guide to access policy structure 266 </a> 267 </Fragment> 268 } 269 /> 270 </Grid> 271 </Grid> 272 )} 273 274 <Box 275 sx={{ 276 marginBottom: "15px", 277 marginTop: "15px", 278 width: "100%", 279 "& label": { width: "180px" }, 280 }} 281 > 282 <DateTimeInput 283 noLabelMinWidth 284 value={expiry} 285 onChange={(e) => { 286 setExpiry(e); 287 }} 288 id="expiryTime" 289 label={"Expiry"} 290 timeFormat={"24h"} 291 secondsSelector={false} 292 /> 293 </Box> 294 295 <InputBox 296 value={name} 297 label={"Name"} 298 id={"name"} 299 name={"name"} 300 type={"text"} 301 placeholder={"Enter a name"} 302 onChange={(e) => { 303 setName(e.target.value); 304 }} 305 /> 306 <InputBox 307 value={description} 308 label={"Description"} 309 id={"description"} 310 name={"description"} 311 type={"text"} 312 placeholder={"Enter a description"} 313 onChange={(e) => { 314 setDescription(e.target.value); 315 }} 316 /> 317 <InputBox 318 value={comments} 319 label={"Comments"} 320 id={"comment"} 321 name={"comment"} 322 type={"text"} 323 placeholder={"Enter a comment"} 324 onChange={(e) => { 325 setComments(e.target.value); 326 }} 327 /> 328 <Grid item xs={12} sx={{ ...modalStyleUtils.modalButtonBar }}> 329 <Button 330 id={"clear"} 331 type="button" 332 variant="regular" 333 onClick={resetForm} 334 label={"Clear"} 335 /> 336 337 <Button 338 id={"create-sa"} 339 type="submit" 340 variant="callAction" 341 color="primary" 342 label={"Create"} 343 /> 344 </Grid> 345 </form> 346 </FormLayout> 347 </PageLayout> 348 </Grid> 349 </Fragment> 350 ); 351 }; 352 353 export default AddServiceAccount;