show service account status and expiry in ui (#2992)

This commit is contained in:
Prakash Senthil Vel
2023-08-16 23:25:07 +05:30
committed by GitHub
parent a2ba20e12f
commit e7fb205c31
14 changed files with 317 additions and 84 deletions

View File

@@ -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)
}) })
} }

View File

@@ -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) {

View File

@@ -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
} }

View File

@@ -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,

View File

@@ -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 }}>

View 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" },
];

View File

@@ -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();

View File

@@ -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 || "";

View File

@@ -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);
}) })

View File

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

View File

@@ -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": {

View File

@@ -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

View File

@@ -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) {

View File

@@ -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: