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;