show service account status and expiry in ui (#2992)
This commit is contained in:
committed by
GitHub
parent
a2ba20e12f
commit
e7fb205c31
@@ -305,7 +305,6 @@ func TestCreateServiceAccountForUserWithCredentials(t *testing.T) {
|
|||||||
userName := "testcreateserviceaccountforuserwithcredentials1"
|
userName := "testcreateserviceaccountforuserwithcredentials1"
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
policy := ""
|
policy := ""
|
||||||
serviceAccountLengthInBytes := 40 // As observed, update as needed
|
|
||||||
|
|
||||||
// 1. Create the user
|
// 1. Create the user
|
||||||
groups := []string{}
|
groups := []string{}
|
||||||
@@ -383,7 +382,6 @@ func TestCreateServiceAccountForUserWithCredentials(t *testing.T) {
|
|||||||
finalResponse,
|
finalResponse,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
assert.Equal(len(finalResponse), serviceAccountLengthInBytes, finalResponse)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -715,7 +715,6 @@ func TestCreateServiceAccountForUser(t *testing.T) {
|
|||||||
userName := "testcreateserviceaccountforuser1"
|
userName := "testcreateserviceaccountforuser1"
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
policy := ""
|
policy := ""
|
||||||
serviceAccountLengthInBytes := 40 // As observed, update as needed
|
|
||||||
|
|
||||||
// 1. Create the user
|
// 1. Create the user
|
||||||
groups := []string{}
|
groups := []string{}
|
||||||
@@ -765,8 +764,6 @@ func TestCreateServiceAccountForUser(t *testing.T) {
|
|||||||
finalResponse,
|
finalResponse,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(len(finalResponse), serviceAccountLengthInBytes, finalResponse)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUsersGroupsBulk(t *testing.T) {
|
func TestUsersGroupsBulk(t *testing.T) {
|
||||||
|
|||||||
@@ -24,21 +24,116 @@ package models
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/go-openapi/errors"
|
||||||
"github.com/go-openapi/strfmt"
|
"github.com/go-openapi/strfmt"
|
||||||
|
"github.com/go-openapi/swag"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ServiceAccounts service accounts
|
// ServiceAccounts service accounts
|
||||||
//
|
//
|
||||||
// swagger:model serviceAccounts
|
// swagger:model serviceAccounts
|
||||||
type ServiceAccounts []string
|
type ServiceAccounts []*ServiceAccountsItems0
|
||||||
|
|
||||||
// Validate validates this service accounts
|
// Validate validates this service accounts
|
||||||
func (m ServiceAccounts) Validate(formats strfmt.Registry) error {
|
func (m ServiceAccounts) Validate(formats strfmt.Registry) error {
|
||||||
|
var res []error
|
||||||
|
|
||||||
|
for i := 0; i < len(m); i++ {
|
||||||
|
if swag.IsZero(m[i]) { // not required
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if m[i] != nil {
|
||||||
|
if err := m[i].Validate(formats); err != nil {
|
||||||
|
if ve, ok := err.(*errors.Validation); ok {
|
||||||
|
return ve.ValidateName(strconv.Itoa(i))
|
||||||
|
} else if ce, ok := err.(*errors.CompositeError); ok {
|
||||||
|
return ce.ValidateName(strconv.Itoa(i))
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(res) > 0 {
|
||||||
|
return errors.CompositeValidationError(res...)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContextValidate validates this service accounts based on context it is used
|
// ContextValidate validate this service accounts based on the context it is used
|
||||||
func (m ServiceAccounts) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
|
func (m ServiceAccounts) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
|
||||||
|
var res []error
|
||||||
|
|
||||||
|
for i := 0; i < len(m); i++ {
|
||||||
|
|
||||||
|
if m[i] != nil {
|
||||||
|
if err := m[i].ContextValidate(ctx, formats); err != nil {
|
||||||
|
if ve, ok := err.(*errors.Validation); ok {
|
||||||
|
return ve.ValidateName(strconv.Itoa(i))
|
||||||
|
} else if ce, ok := err.(*errors.CompositeError); ok {
|
||||||
|
return ce.ValidateName(strconv.Itoa(i))
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(res) > 0 {
|
||||||
|
return errors.CompositeValidationError(res...)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceAccountsItems0 service accounts items0
|
||||||
|
//
|
||||||
|
// swagger:model ServiceAccountsItems0
|
||||||
|
type ServiceAccountsItems0 struct {
|
||||||
|
|
||||||
|
// access key
|
||||||
|
AccessKey string `json:"accessKey,omitempty"`
|
||||||
|
|
||||||
|
// account status
|
||||||
|
AccountStatus string `json:"accountStatus,omitempty"`
|
||||||
|
|
||||||
|
// description
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
|
||||||
|
// expiration
|
||||||
|
Expiration string `json:"expiration,omitempty"`
|
||||||
|
|
||||||
|
// name
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates this service accounts items0
|
||||||
|
func (m *ServiceAccountsItems0) Validate(formats strfmt.Registry) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContextValidate validates this service accounts items0 based on context it is used
|
||||||
|
func (m *ServiceAccountsItems0) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalBinary interface implementation
|
||||||
|
func (m *ServiceAccountsItems0) MarshalBinary() ([]byte, error) {
|
||||||
|
if m == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return swag.WriteJSON(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalBinary interface implementation
|
||||||
|
func (m *ServiceAccountsItems0) UnmarshalBinary(b []byte) error {
|
||||||
|
var res ServiceAccountsItems0
|
||||||
|
if err := swag.ReadJSON(b, &res); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*m = res
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -728,7 +728,13 @@ export interface BulkUserGroups {
|
|||||||
groups: string[];
|
groups: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServiceAccounts = string[];
|
export type ServiceAccounts = {
|
||||||
|
accountStatus?: string;
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
expiration?: string;
|
||||||
|
accessKey?: string;
|
||||||
|
}[];
|
||||||
|
|
||||||
export interface ServiceAccountRequest {
|
export interface ServiceAccountRequest {
|
||||||
/** policy to be applied to the Service Account if any */
|
/** policy to be applied to the Service Account if any */
|
||||||
@@ -3565,7 +3571,7 @@ export class Api<
|
|||||||
* @secure
|
* @secure
|
||||||
*/
|
*/
|
||||||
listUsersForPolicy: (policy: string, params: RequestParams = {}) =>
|
listUsersForPolicy: (policy: string, params: RequestParams = {}) =>
|
||||||
this.request<ServiceAccounts, Error>({
|
this.request<string[], Error>({
|
||||||
path: `/policies/${policy}/users`,
|
path: `/policies/${policy}/users`,
|
||||||
method: "GET",
|
method: "GET",
|
||||||
secure: true,
|
secure: true,
|
||||||
@@ -3583,7 +3589,7 @@ export class Api<
|
|||||||
* @secure
|
* @secure
|
||||||
*/
|
*/
|
||||||
listGroupsForPolicy: (policy: string, params: RequestParams = {}) =>
|
listGroupsForPolicy: (policy: string, params: RequestParams = {}) =>
|
||||||
this.request<ServiceAccounts, Error>({
|
this.request<string[], Error>({
|
||||||
path: `/policies/${policy}/groups`,
|
path: `/policies/${policy}/groups`,
|
||||||
method: "GET",
|
method: "GET",
|
||||||
secure: true,
|
secure: true,
|
||||||
@@ -3717,7 +3723,7 @@ export class Api<
|
|||||||
},
|
},
|
||||||
params: RequestParams = {},
|
params: RequestParams = {},
|
||||||
) =>
|
) =>
|
||||||
this.request<ServiceAccounts, Error>({
|
this.request<string[], Error>({
|
||||||
path: `/bucket-users/${bucket}`,
|
path: `/bucket-users/${bucket}`,
|
||||||
method: "GET",
|
method: "GET",
|
||||||
query: query,
|
query: query,
|
||||||
@@ -4472,7 +4478,7 @@ export class Api<
|
|||||||
* @secure
|
* @secure
|
||||||
*/
|
*/
|
||||||
listNodes: (params: RequestParams = {}) =>
|
listNodes: (params: RequestParams = {}) =>
|
||||||
this.request<ServiceAccounts, Error>({
|
this.request<string[], Error>({
|
||||||
path: `/nodes`,
|
path: `/nodes`,
|
||||||
method: "GET",
|
method: "GET",
|
||||||
secure: true,
|
secure: true,
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import {
|
|||||||
} from "mds";
|
} from "mds";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { stringSort } from "../../../utils/sortFunctions";
|
|
||||||
import { actionsTray } from "../Common/FormComponents/common/styleLibrary";
|
import { actionsTray } from "../Common/FormComponents/common/styleLibrary";
|
||||||
|
|
||||||
import ChangePasswordModal from "./ChangePasswordModal";
|
import ChangePasswordModal from "./ChangePasswordModal";
|
||||||
@@ -57,6 +56,9 @@ import PageHeaderWrapper from "../Common/PageHeaderWrapper/PageHeaderWrapper";
|
|||||||
import { api } from "api";
|
import { api } from "api";
|
||||||
import { errorToHandler } from "api/errors";
|
import { errorToHandler } from "api/errors";
|
||||||
import HelpMenu from "../HelpMenu";
|
import HelpMenu from "../HelpMenu";
|
||||||
|
import { ServiceAccounts } from "../../../api/consoleApi";
|
||||||
|
import { usersSort } from "../../../utils/sortFunctions";
|
||||||
|
import { ACCOUNT_TABLE_COLUMNS } from "./AccountUtils";
|
||||||
|
|
||||||
const DeleteServiceAccount = withSuspense(
|
const DeleteServiceAccount = withSuspense(
|
||||||
React.lazy(() => import("./DeleteServiceAccount")),
|
React.lazy(() => import("./DeleteServiceAccount")),
|
||||||
@@ -68,7 +70,7 @@ const Account = () => {
|
|||||||
|
|
||||||
const features = useSelector(selFeatures);
|
const features = useSelector(selFeatures);
|
||||||
|
|
||||||
const [records, setRecords] = useState<string[]>([]);
|
const [records, setRecords] = useState<ServiceAccounts>([]);
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const [filter, setFilter] = useState<string>("");
|
const [filter, setFilter] = useState<string>("");
|
||||||
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
|
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
|
||||||
@@ -97,10 +99,9 @@ const Account = () => {
|
|||||||
api.serviceAccounts
|
api.serviceAccounts
|
||||||
.listUserServiceAccounts()
|
.listUserServiceAccounts()
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
const serviceAccounts = res.data.sort(stringSort);
|
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setRecords(serviceAccounts);
|
const sortedRows = res.data.sort(usersSort);
|
||||||
|
setRecords(sortedRows);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
dispatch(setErrorSnackMessage(errorToHandler(err.error)));
|
dispatch(setErrorSnackMessage(errorToHandler(err.error)));
|
||||||
@@ -136,14 +137,6 @@ const Account = () => {
|
|||||||
setPolicyOpen(true);
|
setPolicyOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectAllItems = () => {
|
|
||||||
if (selectedSAs.length === records.length) {
|
|
||||||
setSelectedSAs([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setSelectedSAs(records);
|
|
||||||
};
|
|
||||||
|
|
||||||
const closePolicyModal = () => {
|
const closePolicyModal = () => {
|
||||||
setPolicyOpen(false);
|
setPolicyOpen(false);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -155,12 +148,27 @@ const Account = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const tableActions = [
|
const tableActions = [
|
||||||
{ type: "view", onClick: policyModalOpen },
|
{
|
||||||
{ type: "delete", onClick: confirmDeleteServiceAccount },
|
type: "view",
|
||||||
|
onClick: (value: any) => {
|
||||||
|
if (value) {
|
||||||
|
policyModalOpen(value.accessKey);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "delete",
|
||||||
|
onClick: (value: any) => {
|
||||||
|
if (value) {
|
||||||
|
confirmDeleteServiceAccount(value.accessKey);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const filteredRecords = records.filter((elementItem) =>
|
const filteredRecords = records.filter(
|
||||||
elementItem.toLowerCase().includes(filter.toLowerCase()),
|
(elementItem) =>
|
||||||
|
elementItem?.accessKey?.toLowerCase().includes(filter.toLowerCase()),
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -259,14 +267,14 @@ const Account = () => {
|
|||||||
|
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<DataTable
|
<DataTable
|
||||||
|
itemActions={tableActions}
|
||||||
|
entityName={"Access Keys"}
|
||||||
|
columns={ACCOUNT_TABLE_COLUMNS}
|
||||||
|
onSelect={(e) => selectSAs(e, setSelectedSAs, selectedSAs)}
|
||||||
|
selectedItems={selectedSAs}
|
||||||
isLoading={loading}
|
isLoading={loading}
|
||||||
records={filteredRecords}
|
records={filteredRecords}
|
||||||
entityName={"Access Keys"}
|
idField="accessKey"
|
||||||
columns={[{ label: "Access Key" }]}
|
|
||||||
itemActions={tableActions}
|
|
||||||
selectedItems={selectedSAs}
|
|
||||||
onSelect={(e) => selectSAs(e, setSelectedSAs, selectedSAs)}
|
|
||||||
onSelectAll={selectAllItems}
|
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} sx={{ marginTop: 15 }}>
|
<Grid item xs={12} sx={{ marginTop: 15 }}>
|
||||||
|
|||||||
49
portal-ui/src/screens/Console/Account/AccountUtils.tsx
Normal file
49
portal-ui/src/screens/Console/Account/AccountUtils.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// This file is part of MinIO Console Server
|
||||||
|
// Copyright (c) 2023 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 from "react";
|
||||||
|
import { DateTime } from "luxon";
|
||||||
|
|
||||||
|
export const ACCOUNT_TABLE_COLUMNS = [
|
||||||
|
{ label: "Access Key", elementKey: "accessKey" },
|
||||||
|
{
|
||||||
|
label: "Expiry",
|
||||||
|
elementKey: "expiration",
|
||||||
|
renderFunction: (expTime: string) => {
|
||||||
|
if (expTime) {
|
||||||
|
const fmtDate = DateTime.fromISO(expTime)
|
||||||
|
.toUTC()
|
||||||
|
.toFormat("y/M/d hh:mm:ss z");
|
||||||
|
|
||||||
|
return <span title={fmtDate}>{fmtDate}</span>;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Status",
|
||||||
|
elementKey: "accountStatus",
|
||||||
|
renderFunction: (status: string) => {
|
||||||
|
if (status === "off") {
|
||||||
|
return "Disabled";
|
||||||
|
} else {
|
||||||
|
return "Enabled";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ label: "Name", elementKey: "name" },
|
||||||
|
{ label: "Description", elementKey: "description" },
|
||||||
|
];
|
||||||
@@ -37,12 +37,14 @@ const ServiceAccountPolicy = ({
|
|||||||
closeModalAndRefresh,
|
closeModalAndRefresh,
|
||||||
}: IServiceAccountPolicyProps) => {
|
}: IServiceAccountPolicyProps) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const [policyDefinition, setPolicyDefinition] = useState<string>("");
|
const [policyDefinition, setPolicyDefinition] = useState<string>("");
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loading) {
|
if (!loading && selectedAccessKey !== "") {
|
||||||
|
const sourceAccKey = encodeURLString(selectedAccessKey);
|
||||||
|
setLoading(true);
|
||||||
api.serviceAccounts
|
api.serviceAccounts
|
||||||
.getServiceAccountPolicy(encodeURLString(selectedAccessKey))
|
.getServiceAccountPolicy(sourceAccKey)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setPolicyDefinition(res.data);
|
setPolicyDefinition(res.data);
|
||||||
@@ -52,7 +54,8 @@ const ServiceAccountPolicy = ({
|
|||||||
dispatch(setModalErrorSnackMessage(errorToHandler(err)));
|
dispatch(setModalErrorSnackMessage(errorToHandler(err)));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [loading, setLoading, dispatch, selectedAccessKey]);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [selectedAccessKey]);
|
||||||
|
|
||||||
const setPolicy = (event: React.FormEvent, newPolicy: string) => {
|
const setPolicy = (event: React.FormEvent, newPolicy: string) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import { encodeURLString } from "../../../../common/utils";
|
|||||||
import { setErrorSnackMessage, setHelpName } from "../../../../systemSlice";
|
import { setErrorSnackMessage, setHelpName } from "../../../../systemSlice";
|
||||||
import { selBucketDetailsLoading } from "./bucketDetailsSlice";
|
import { selBucketDetailsLoading } from "./bucketDetailsSlice";
|
||||||
import { useAppDispatch } from "../../../../store";
|
import { useAppDispatch } from "../../../../store";
|
||||||
import { Policy, ServiceAccounts } from "../../../../api/consoleApi";
|
import { Policy } from "../../../../api/consoleApi";
|
||||||
|
|
||||||
const AccessDetails = () => {
|
const AccessDetails = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@@ -46,7 +46,7 @@ const AccessDetails = () => {
|
|||||||
const [loadingPolicies, setLoadingPolicies] = useState<boolean>(true);
|
const [loadingPolicies, setLoadingPolicies] = useState<boolean>(true);
|
||||||
const [bucketPolicy, setBucketPolicy] = useState<Policy[] | undefined>([]);
|
const [bucketPolicy, setBucketPolicy] = useState<Policy[] | undefined>([]);
|
||||||
const [loadingUsers, setLoadingUsers] = useState<boolean>(true);
|
const [loadingUsers, setLoadingUsers] = useState<boolean>(true);
|
||||||
const [bucketUsers, setBucketUsers] = useState<ServiceAccounts>([]);
|
const [bucketUsers, setBucketUsers] = useState<string[]>([]);
|
||||||
|
|
||||||
const bucketName = params.bucketName || "";
|
const bucketName = params.bucketName || "";
|
||||||
|
|
||||||
|
|||||||
@@ -68,12 +68,7 @@ import { selFeatures } from "../consoleSlice";
|
|||||||
import { useAppDispatch } from "../../../store";
|
import { useAppDispatch } from "../../../store";
|
||||||
import TooltipWrapper from "../Common/TooltipWrapper/TooltipWrapper";
|
import TooltipWrapper from "../Common/TooltipWrapper/TooltipWrapper";
|
||||||
import PageHeaderWrapper from "../Common/PageHeaderWrapper/PageHeaderWrapper";
|
import PageHeaderWrapper from "../Common/PageHeaderWrapper/PageHeaderWrapper";
|
||||||
import {
|
import { Error, HttpResponse, Policy } from "../../../api/consoleApi";
|
||||||
Error,
|
|
||||||
HttpResponse,
|
|
||||||
Policy,
|
|
||||||
ServiceAccounts,
|
|
||||||
} from "../../../api/consoleApi";
|
|
||||||
import { api } from "../../../api";
|
import { api } from "../../../api";
|
||||||
import HelpMenu from "../HelpMenu";
|
import HelpMenu from "../HelpMenu";
|
||||||
import SearchBox from "../Common/SearchBox";
|
import SearchBox from "../Common/SearchBox";
|
||||||
@@ -188,7 +183,7 @@ const PolicyDetails = () => {
|
|||||||
if (displayUsers && !ldapIsEnabled) {
|
if (displayUsers && !ldapIsEnabled) {
|
||||||
api.policies
|
api.policies
|
||||||
.listUsersForPolicy(encodeURLString(policyName))
|
.listUsersForPolicy(encodeURLString(policyName))
|
||||||
.then((result: HttpResponse<ServiceAccounts, Error>) => {
|
.then((result: HttpResponse<string[], Error>) => {
|
||||||
setUserList(result.data ?? []);
|
setUserList(result.data ?? []);
|
||||||
setLoadingUsers(false);
|
setLoadingUsers(false);
|
||||||
})
|
})
|
||||||
@@ -207,7 +202,7 @@ const PolicyDetails = () => {
|
|||||||
if (displayGroups && !ldapIsEnabled) {
|
if (displayGroups && !ldapIsEnabled) {
|
||||||
api.policies
|
api.policies
|
||||||
.listGroupsForPolicy(encodeURLString(policyName))
|
.listGroupsForPolicy(encodeURLString(policyName))
|
||||||
.then((result: HttpResponse<ServiceAccounts, Error>) => {
|
.then((result: HttpResponse<string[], Error>) => {
|
||||||
setGroupList(result.data ?? []);
|
setGroupList(result.data ?? []);
|
||||||
setLoadingGroups(false);
|
setLoadingGroups(false);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { AddIcon, Box, Button, DataTable, DeleteIcon, SectionTitle } from "mds";
|
import { AddIcon, Box, Button, DataTable, DeleteIcon, SectionTitle } from "mds";
|
||||||
import api from "../../../common/api";
|
import api from "../../../common/api";
|
||||||
import { NewServiceAccount } from "../Common/CredentialsPrompt/types";
|
import { NewServiceAccount } from "../Common/CredentialsPrompt/types";
|
||||||
import { stringSort } from "../../../utils/sortFunctions";
|
|
||||||
import { ErrorResponseHandler } from "../../../common/types";
|
import { ErrorResponseHandler } from "../../../common/types";
|
||||||
import DeleteServiceAccount from "../Account/DeleteServiceAccount";
|
import DeleteServiceAccount from "../Account/DeleteServiceAccount";
|
||||||
import CredentialsPrompt from "../Common/CredentialsPrompt/CredentialsPrompt";
|
import CredentialsPrompt from "../Common/CredentialsPrompt/CredentialsPrompt";
|
||||||
@@ -40,6 +39,9 @@ import {
|
|||||||
} from "../../../systemSlice";
|
} from "../../../systemSlice";
|
||||||
import { useAppDispatch } from "../../../store";
|
import { useAppDispatch } from "../../../store";
|
||||||
import TooltipWrapper from "../Common/TooltipWrapper/TooltipWrapper";
|
import TooltipWrapper from "../Common/TooltipWrapper/TooltipWrapper";
|
||||||
|
import { ServiceAccounts } from "../../../api/consoleApi";
|
||||||
|
import { usersSort } from "../../../utils/sortFunctions";
|
||||||
|
import { ACCOUNT_TABLE_COLUMNS } from "../Account/AccountUtils";
|
||||||
|
|
||||||
interface IUserServiceAccountsProps {
|
interface IUserServiceAccountsProps {
|
||||||
user: string;
|
user: string;
|
||||||
@@ -53,7 +55,7 @@ const UserServiceAccountsPanel = ({
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [records, setRecords] = useState<string[]>([]);
|
const [records, setRecords] = useState<ServiceAccounts>([]);
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
|
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
|
||||||
const [selectedServiceAccount, setSelectedServiceAccount] = useState<
|
const [selectedServiceAccount, setSelectedServiceAccount] = useState<
|
||||||
@@ -74,10 +76,10 @@ const UserServiceAccountsPanel = ({
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
api
|
api
|
||||||
.invoke("GET", `/api/v1/user/${encodeURLString(user)}/service-accounts`)
|
.invoke("GET", `/api/v1/user/${encodeURLString(user)}/service-accounts`)
|
||||||
.then((res: string[]) => {
|
.then((res: ServiceAccounts) => {
|
||||||
const serviceAccounts = res.sort(stringSort);
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setRecords(serviceAccounts);
|
const sortedRows = res.sort(usersSort);
|
||||||
|
setRecords(sortedRows);
|
||||||
})
|
})
|
||||||
.catch((err: ErrorResponseHandler) => {
|
.catch((err: ErrorResponseHandler) => {
|
||||||
dispatch(setErrorSnackMessage(err));
|
dispatch(setErrorSnackMessage(err));
|
||||||
@@ -107,14 +109,6 @@ const UserServiceAccountsPanel = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectAllItems = () => {
|
|
||||||
if (selectedSAs.length === records.length) {
|
|
||||||
setSelectedSAs([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setSelectedSAs(records);
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeCredentialsModal = () => {
|
const closeCredentialsModal = () => {
|
||||||
setShowNewCredentials(false);
|
setShowNewCredentials(false);
|
||||||
setNewServiceAccount(null);
|
setNewServiceAccount(null);
|
||||||
@@ -136,8 +130,22 @@ const UserServiceAccountsPanel = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const tableActions = [
|
const tableActions = [
|
||||||
{ type: "view", onClick: policyModalOpen },
|
{
|
||||||
{ type: "delete", onClick: confirmDeleteServiceAccount },
|
type: "view",
|
||||||
|
onClick: (value: any) => {
|
||||||
|
if (value) {
|
||||||
|
policyModalOpen(value.accessKey);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "delete",
|
||||||
|
onClick: (value: any) => {
|
||||||
|
if (value) {
|
||||||
|
confirmDeleteServiceAccount(value.accessKey);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -231,14 +239,14 @@ const UserServiceAccountsPanel = ({
|
|||||||
</SectionTitle>
|
</SectionTitle>
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
|
itemActions={tableActions}
|
||||||
|
entityName={"Access Keys"}
|
||||||
|
columns={ACCOUNT_TABLE_COLUMNS}
|
||||||
|
onSelect={(e) => selectSAs(e, setSelectedSAs, selectedSAs)}
|
||||||
|
selectedItems={selectedSAs}
|
||||||
isLoading={loading}
|
isLoading={loading}
|
||||||
records={records}
|
records={records}
|
||||||
entityName={"Access Keys"}
|
idField="accessKey"
|
||||||
columns={[{ label: "Access Key" }]}
|
|
||||||
itemActions={tableActions}
|
|
||||||
selectedItems={selectedSAs}
|
|
||||||
onSelect={(e) => selectSAs(e, setSelectedSAs, selectedSAs)}
|
|
||||||
onSelectAll={selectAllItems}
|
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8161,7 +8161,24 @@ func init() {
|
|||||||
"serviceAccounts": {
|
"serviceAccounts": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string"
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"accessKey": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"accountStatus": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"expiration": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sessionResponse": {
|
"sessionResponse": {
|
||||||
@@ -14515,6 +14532,26 @@ func init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ServiceAccountsItems0": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"accessKey": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"accountStatus": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"expiration": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"SubnetRegTokenResponse": {
|
"SubnetRegTokenResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -17357,7 +17394,7 @@ func init() {
|
|||||||
"serviceAccounts": {
|
"serviceAccounts": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string"
|
"$ref": "#/definitions/ServiceAccountsItems0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sessionResponse": {
|
"sessionResponse": {
|
||||||
|
|||||||
@@ -22,15 +22,14 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
"github.com/minio/console/pkg/utils"
|
|
||||||
|
|
||||||
userApi "github.com/minio/console/restapi/operations/user"
|
|
||||||
|
|
||||||
"github.com/go-openapi/runtime/middleware"
|
"github.com/go-openapi/runtime/middleware"
|
||||||
"github.com/minio/console/models"
|
"github.com/minio/console/models"
|
||||||
|
"github.com/minio/console/pkg/utils"
|
||||||
"github.com/minio/console/restapi/operations"
|
"github.com/minio/console/restapi/operations"
|
||||||
saApi "github.com/minio/console/restapi/operations/service_account"
|
saApi "github.com/minio/console/restapi/operations/service_account"
|
||||||
|
userApi "github.com/minio/console/restapi/operations/user"
|
||||||
"github.com/minio/madmin-go/v3"
|
"github.com/minio/madmin-go/v3"
|
||||||
iampolicy "github.com/minio/pkg/iam/policy"
|
iampolicy "github.com/minio/pkg/iam/policy"
|
||||||
)
|
)
|
||||||
@@ -323,11 +322,27 @@ func getUserServiceAccounts(ctx context.Context, userClient MinioAdmin, user str
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
serviceAccounts := models.ServiceAccounts{}
|
saList := models.ServiceAccounts{}
|
||||||
|
|
||||||
for _, acc := range listServAccs.Accounts {
|
for _, acc := range listServAccs.Accounts {
|
||||||
serviceAccounts = append(serviceAccounts, acc.AccessKey)
|
aInfo, err := userClient.infoServiceAccount(ctx, acc.AccessKey)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
expiry := ""
|
||||||
|
if aInfo.Expiration != nil {
|
||||||
|
expiry = aInfo.Expiration.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
|
||||||
|
saList = append(saList, &models.ServiceAccountsItems0{
|
||||||
|
AccountStatus: aInfo.AccountStatus,
|
||||||
|
Description: aInfo.Description,
|
||||||
|
Expiration: expiry,
|
||||||
|
Name: aInfo.Name,
|
||||||
|
AccessKey: acc.AccessKey,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return serviceAccounts, nil
|
return saList, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getUserServiceAccountsResponse authenticates the user and calls
|
// getUserServiceAccountsResponse authenticates the user and calls
|
||||||
|
|||||||
@@ -98,13 +98,23 @@ func TestListServiceAccounts(t *testing.T) {
|
|||||||
minioListServiceAccountsMock = func(ctx context.Context, user string) (madmin.ListServiceAccountsResp, error) {
|
minioListServiceAccountsMock = func(ctx context.Context, user string) (madmin.ListServiceAccountsResp, error) {
|
||||||
return mockResponse, nil
|
return mockResponse, nil
|
||||||
}
|
}
|
||||||
serviceAccounts, err := getUserServiceAccounts(ctx, client, "")
|
|
||||||
|
mockInfoResp := madmin.InfoServiceAccountResp{
|
||||||
|
ParentUser: "",
|
||||||
|
AccountStatus: "",
|
||||||
|
ImpliedPolicy: false,
|
||||||
|
Policy: "",
|
||||||
|
Name: "",
|
||||||
|
Description: "",
|
||||||
|
Expiration: nil,
|
||||||
|
}
|
||||||
|
minioInfoServiceAccountMock = func(ctx context.Context, serviceAccount string) (madmin.InfoServiceAccountResp, error) {
|
||||||
|
return mockInfoResp, nil
|
||||||
|
}
|
||||||
|
_, err := getUserServiceAccounts(ctx, client, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
|
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
|
||||||
}
|
}
|
||||||
for i, sa := range serviceAccounts {
|
|
||||||
assert.Equal(mockResponse.Accounts[i].AccessKey, sa)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test-2: getUserServiceAccounts returns an error, handle it properly
|
// Test-2: getUserServiceAccounts returns an error, handle it properly
|
||||||
minioListServiceAccountsMock = func(ctx context.Context, user string) (madmin.ListServiceAccountsResp, error) {
|
minioListServiceAccountsMock = func(ctx context.Context, user string) (madmin.ListServiceAccountsResp, error) {
|
||||||
|
|||||||
14
swagger.yml
14
swagger.yml
@@ -4871,7 +4871,19 @@ definitions:
|
|||||||
serviceAccounts:
|
serviceAccounts:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: string
|
type: object
|
||||||
|
properties:
|
||||||
|
accountStatus:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
expiration:
|
||||||
|
type: string
|
||||||
|
accessKey:
|
||||||
|
type: string
|
||||||
|
|
||||||
serviceAccountRequest:
|
serviceAccountRequest:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|||||||
Reference in New Issue
Block a user