Skip to content

Commit

Permalink
[CLOUDGA-25138] Show api keys associated to an allow list in LIST AL …
Browse files Browse the repository at this point in the history
…cmd (#290)

* [CLOUDGA-25138] Show api keys associated to an allow list in LIST AL cmd

* [CLOUDGA-25138] remove API_KEY_ALLOW_LIST FF

* [CLOUDGA-25138] Added doc
  • Loading branch information
bhupendray-yb authored Jan 20, 2025
1 parent 3803659 commit 9d9ba9a
Show file tree
Hide file tree
Showing 6 changed files with 60 additions and 124 deletions.
27 changes: 11 additions & 16 deletions cmd/api_key/api_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,22 +98,19 @@ func addAllowListNameToApiKeyData(apiKeys *[]ybmclient.ApiKeyData, authApi *ybmA
apiKeyOutputList := make([]formatter.ApiKeyDataAllowListInfo, 0)

// For each API key, fetch the allow list(s) associated with it
isApiKeyAllowListEnabled := util.IsFeatureFlagEnabled(util.API_KEY_ALLOW_LIST)
for _, apiKey := range *apiKeys {
apiKeyId := apiKey.GetInfo().Id
allowListsNames := make([]string, 0)

if isApiKeyAllowListEnabled {
allowListIds := apiKey.GetSpec().AllowListInfo
if allowListIds != nil && len(*allowListIds) > 0 {
apiKeyAllowLists, resp, err := authApi.ListApiKeyNetworkAllowLists(apiKeyId).Execute()
if err != nil {
logrus.Debugf("Full HTTP response: %v", resp)
logrus.Fatalf(ybmAuthClient.GetApiErrorDetails(err))
}
for _, allowList := range apiKeyAllowLists.GetData() {
allowListsNames = append(allowListsNames, allowList.GetSpec().Name)
}
allowListIds := apiKey.GetSpec().AllowListInfo
if allowListIds != nil && len(*allowListIds) > 0 {
apiKeyAllowLists, resp, err := authApi.ListApiKeyNetworkAllowLists(apiKeyId).Execute()
if err != nil {
logrus.Debugf("Full HTTP response: %v", resp)
logrus.Fatalf(ybmAuthClient.GetApiErrorDetails(err))
}
for _, allowList := range apiKeyAllowLists.GetData() {
allowListsNames = append(allowListsNames, allowList.GetSpec().Name)
}
}

Expand Down Expand Up @@ -197,7 +194,7 @@ var createApiKeyCmd = &cobra.Command{
apiKeySpec.SetRoleId(roleId)
}

if util.IsFeatureFlagEnabled(util.API_KEY_ALLOW_LIST) && cmd.Flags().Changed("network-allow-lists") {
if cmd.Flags().Changed("network-allow-lists") {
allowLists, _ := cmd.Flags().GetString("network-allow-lists")

allowListNames := strings.Split(allowLists, ",")
Expand Down Expand Up @@ -284,9 +281,7 @@ func init() {
createApiKeyCmd.MarkFlagRequired("unit")
createApiKeyCmd.Flags().String("description", "", "[OPTIONAL] Description of the API Key to be created.")

if util.IsFeatureFlagEnabled(util.API_KEY_ALLOW_LIST) {
createApiKeyCmd.Flags().String("network-allow-lists", "", "[OPTIONAL] The network allow lists(comma separated names) to assign to the API key.")
}
createApiKeyCmd.Flags().String("network-allow-lists", "", "[OPTIONAL] The network allow lists(comma separated names) to assign to the API key.")
createApiKeyCmd.Flags().String("role-name", "", "[OPTIONAL] The name of the role to be assigned to the API Key. If not provided, an Admin API Key will be generated.")
createApiKeyCmd.Flags().BoolP("force", "f", false, "Bypass the prompt for non-interactive usage")

Expand Down
119 changes: 26 additions & 93 deletions cmd/api_key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,137 +43,70 @@ var _ = Describe("API Key Management", func() {
server.Close()
})

Describe("When listing API keys with allow list FF disabled", func() {
It("should hide allow list column", func() {
err := loadJson("./test/fixtures/list-api-keys.json", &apiKeyListResponse)
Describe("When listing API keys", func() {
It("should list api keys", func() {
err := loadJson("./test/fixtures/list-api-keys-with-allow-list.json", &apiKeyListResponse)
Expect(err).ToNot(HaveOccurred())
err = loadJson("./test/fixtures/allow-list.json", &responseNetworkAllowList)
Expect(err).ToNot(HaveOccurred())

server.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest(http.MethodGet, "/api/public/v1/accounts/340af43a-8a7c-4659-9258-4876fd6a207b/api-keys"),
ghttp.RespondWithJSONEncodedPtr(&statusCode, apiKeyListResponse),
),
ghttp.CombineHandlers(
ghttp.VerifyRequest(http.MethodGet, "/api/public/v1/accounts/340af43a-8a7c-4659-9258-4876fd6a207b/projects/78d4459c-0f45-47a5-899a-45ddf43eba6e/allow-lists"),
ghttp.RespondWithJSONEncodedPtr(&statusCode, responseNetworkAllowList),
),
)
os.Setenv("YBM_FF_API_KEY_ALLOW_LIST", "false")

cmd := exec.Command(compiledCLIPath, "api-key", "list")
session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter)

Expect(err).NotTo(HaveOccurred())
session.Wait(2)
Expect(session.Out).Should(gbytes.Say(`Name Role Status Created By Date Created Last Used Expiration
apikey-1 Admin ACTIVE [email protected] 2025-01-13T13:55:14.024Z 2025-01-13T14:21:40.713599Z 2099-12-31T00:00Z
apikey-2 Admin ACTIVE [email protected] 2025-01-08T09:06:35.077Z Not yet used 2025-02-07T09:06:35.077071Z`))
Expect(session.Out).Should(gbytes.Say(`Name Role Status Created By Date Created Last Used Expiration Allow List
apikey-1 Admin ACTIVE [email protected] 2025-01-13T13:55:14.024Z 2025-01-13T14:21:40.713599Z 2099-12-31T00:00Z device-ip-gween
apikey-2 Admin ACTIVE [email protected] 2025-01-08T09:06:35.077Z Not yet used 2025-02-07T09:06:35.077071Z N/A`))
session.Kill()
})

})

Describe("When listing API keys with allow list FF enabled", func() {
It("should show allow list column", func() {
err := loadJson("./test/fixtures/list-api-keys-with-allow-list.json", &apiKeyListResponse)
Describe("When creating an API key", func() {
It("should create API key", func() {
err := loadJson("./test/fixtures/get-or-create-api-key.json", &apiKeyResponse)
Expect(err).ToNot(HaveOccurred())
err = loadJson("./test/fixtures/allow-list.json", &responseNetworkAllowList)
Expect(err).ToNot(HaveOccurred())

server.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest(http.MethodGet, "/api/public/v1/accounts/340af43a-8a7c-4659-9258-4876fd6a207b/api-keys"),
ghttp.RespondWithJSONEncodedPtr(&statusCode, apiKeyListResponse),
ghttp.VerifyRequest(http.MethodGet, "/api/public/v1/accounts/340af43a-8a7c-4659-9258-4876fd6a207b/projects/78d4459c-0f45-47a5-899a-45ddf43eba6e/allow-lists"),
ghttp.RespondWithJSONEncodedPtr(&statusCode, responseNetworkAllowList),
),
ghttp.CombineHandlers(
ghttp.VerifyRequest(http.MethodPost, "/api/public/v1/accounts/340af43a-8a7c-4659-9258-4876fd6a207b/api-keys"),
ghttp.RespondWithJSONEncodedPtr(&statusCode, apiKeyResponse),
),
ghttp.CombineHandlers(
ghttp.VerifyRequest(http.MethodGet, "/api/public/v1/accounts/340af43a-8a7c-4659-9258-4876fd6a207b/projects/78d4459c-0f45-47a5-899a-45ddf43eba6e/allow-lists"),
ghttp.RespondWithJSONEncodedPtr(&statusCode, responseNetworkAllowList),
),
)

os.Setenv("YBM_FF_API_KEY_ALLOW_LIST", "true")
cmd := exec.Command(compiledCLIPath, "api-key", "list")
cmd := exec.Command(compiledCLIPath, "api-key", "create", "--name", "apikey-1", "--duration", "30", "--unit", "DAYS", "--network-allow-lists", "device-ip-gween")
session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter)

Expect(err).NotTo(HaveOccurred())
session.Wait(2)
Expect(session.Out).Should(gbytes.Say(`Name Role Status Created By Date Created Last Used Expiration Allow List
apikey-1 Admin ACTIVE [email protected] 2025-01-13T13:55:14.024Z 2025-01-13T14:21:40.713599Z 2099-12-31T00:00Z device-ip-gween
apikey-2 Admin ACTIVE [email protected] 2025-01-08T09:06:35.077Z Not yet used 2025-02-07T09:06:35.077071Z N/A`))
session.Kill()
})

})

Describe("When creating an API key", func() {
Context("with allow list feature flag disabled", func() {
os.Setenv("YBM_FF_API_KEY_ALLOW_LIST", "false")

It("should error when passing allow list flag", func() {
os.Setenv("YBM_FF_API_KEY_ALLOW_LIST", "false")
cmd := exec.Command(compiledCLIPath, "api-key", "create", "--name", "apikey-1", "--duration", "30", "--unit", "DAYS", "--network-allow-lists", "device-ip")
session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter)

Expect(err).NotTo(HaveOccurred())
session.Wait(2)
Expect(session.Err).Should(gbytes.Say(`\bError: unknown flag: --network-allow-lists\b`))
session.Kill()
})

It("should create API key", func() {
err := loadJson("./test/fixtures/get-or-create-api-key.json", &apiKeyResponse)
Expect(err).ToNot(HaveOccurred())

server.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest(http.MethodPost, "/api/public/v1/accounts/340af43a-8a7c-4659-9258-4876fd6a207b/api-keys"),
ghttp.RespondWithJSONEncodedPtr(&statusCode, apiKeyResponse),
),
)

cmd := exec.Command(compiledCLIPath, "api-key", "create", "--name", "apikey-1", "--duration", "30", "--unit", "DAYS")
session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter)

Expect(err).NotTo(HaveOccurred())
session.Wait(2)
Expect(session.Out).Should(gbytes.Say(`Name Role Status Created By Date Created Last Used Expiration
apikey-1 Admin ACTIVE admin 2025-01-13T15:55:55.825Z Not yet used 2025-02-12T15:55:55.824833Z
API Key: test-jwt`))
session.Kill()
})
})

Context("with allow list feature flag enabled", func() {
It("should create API key", func() {
err := loadJson("./test/fixtures/get-or-create-api-key.json", &apiKeyResponse)
Expect(err).ToNot(HaveOccurred())
err = loadJson("./test/fixtures/allow-list.json", &responseNetworkAllowList)
Expect(err).ToNot(HaveOccurred())

server.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest(http.MethodGet, "/api/public/v1/accounts/340af43a-8a7c-4659-9258-4876fd6a207b/projects/78d4459c-0f45-47a5-899a-45ddf43eba6e/allow-lists"),
ghttp.RespondWithJSONEncodedPtr(&statusCode, responseNetworkAllowList),
),
ghttp.CombineHandlers(
ghttp.VerifyRequest(http.MethodPost, "/api/public/v1/accounts/340af43a-8a7c-4659-9258-4876fd6a207b/api-keys"),
ghttp.RespondWithJSONEncodedPtr(&statusCode, apiKeyResponse),
),
ghttp.CombineHandlers(
ghttp.VerifyRequest(http.MethodGet, "/api/public/v1/accounts/340af43a-8a7c-4659-9258-4876fd6a207b/projects/78d4459c-0f45-47a5-899a-45ddf43eba6e/allow-lists"),
ghttp.RespondWithJSONEncodedPtr(&statusCode, responseNetworkAllowList),
),
)

os.Setenv("YBM_FF_API_KEY_ALLOW_LIST", "true")
cmd := exec.Command(compiledCLIPath, "api-key", "create", "--name", "apikey-1", "--duration", "30", "--unit", "DAYS", "--network-allow-lists", "device-ip-gween")
session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter)

Expect(err).NotTo(HaveOccurred())
session.Wait(2)
Expect(session.Out).Should(gbytes.Say(`Name Role Status Created By Date Created Last Used Expiration Allow List
Expect(session.Out).Should(gbytes.Say(`Name Role Status Created By Date Created Last Used Expiration Allow List
apikey-1 Admin ACTIVE admin 2025-01-13T15:55:55.825Z Not yet used 2025-02-12T15:55:55.824833Z device-ip-gween
API Key: test-jwt`))
session.Kill()
})
session.Kill()
})

})

Describe("When revoking an API key", func() {
Expand Down
1 change: 0 additions & 1 deletion cmd/util/feature_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ const (
PITR_RESTORE FeatureFlag = "PITR_RESTORE"
CONNECTION_POOLING FeatureFlag = "CONNECTION_POOLING"
DR FeatureFlag = "DR"
API_KEY_ALLOW_LIST FeatureFlag = "API_KEY_ALLOW_LIST"
)

func (f FeatureFlag) String() string {
Expand Down
16 changes: 9 additions & 7 deletions docs/ybm_api-key_create.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ ybm api-key create [flags]
### Options

```
--name string [REQUIRED] The name of the API Key.
--duration int32 [REQUIRED] The duration for which the API Key will be valid. 0 denotes that the key will never expire.
--unit string [REQUIRED] The time units for which the API Key will be valid. Available options are Hours, Days, and Months.
--description string [OPTIONAL] Description of the API Key to be created.
--role-name string [OPTIONAL] The name of the role to be assigned to the API Key. If not provided, an Admin API Key will be generated.
-f, --force Bypass the prompt for non-interactive usage
-h, --help help for create
--name string [REQUIRED] The name of the API Key.
--duration int32 [REQUIRED] The duration for which the API Key will be valid. 0 denotes that the key will never expire.
--unit string [REQUIRED] The time units for which the API Key will be valid. Available options are Hours, Days, and Months.
--description string [OPTIONAL] Description of the API Key to be created.
--network-allow-lists string [OPTIONAL] The network allow lists(comma separated names) to assign to the API key.
--role-name string [OPTIONAL] The name of the role to be assigned to the API Key. If not provided, an Admin API Key will be generated.
-f, --force Bypass the prompt for non-interactive usage
-h, --help help for create
```

### Options inherited from parent commands
Expand All @@ -28,6 +29,7 @@ ybm api-key create [flags]
-a, --apiKey string YugabyteDB Aeon account API key
--config string config file (default is $HOME/.ybm-cli.yaml)
--debug Use debug mode, same as --logLevel debug
--host string YugabyteDB Aeon Api hostname
-l, --logLevel string Select the desired log level format(info). Default to info
--no-color Disable colors in output , default to false
-o, --output string Select the desired output format (table, json, pretty). Default to table
Expand Down
7 changes: 1 addition & 6 deletions internal/formatter/api_keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,11 @@ import (
"strings"

"github.com/sirupsen/logrus"
"github.com/yugabyte/ybm-cli/cmd/util"
ybmclient "github.com/yugabyte/yugabytedb-managed-go-client-internal"
)

const (
defaultApiKeyListing = "table {{.ApiKeyName}}\t{{.ApiKeyRole}}\t{{.ApiKeyStatus}}\t{{.Issuer}}\t{{.CreatedAt}}\t{{.LastUsed}}\t{{.ExpiryTime}}"
apiKeyListingV2 = "table {{.ApiKeyName}}\t{{.ApiKeyRole}}\t{{.ApiKeyStatus}}\t{{.Issuer}}\t{{.CreatedAt}}\t{{.LastUsed}}\t{{.ExpiryTime}}\t{{.AllowList}}"
defaultApiKeyListing = "table {{.ApiKeyName}}\t{{.ApiKeyRole}}\t{{.ApiKeyStatus}}\t{{.Issuer}}\t{{.CreatedAt}}\t{{.LastUsed}}\t{{.ExpiryTime}}\t{{.AllowList}}"
createdAtHeader = "Date Created"
expiryTimeHeader = "Expiration"
apiKeyRoleHeader = "Role"
Expand All @@ -49,9 +47,6 @@ func NewApiKeyFormat(source string) Format {
switch source {
case "table", "":
format := defaultApiKeyListing
if util.IsFeatureFlagEnabled(util.API_KEY_ALLOW_LIST) {
format = apiKeyListingV2
}
return Format(format)
default: // custom format or json or pretty
return Format(source)
Expand Down
14 changes: 13 additions & 1 deletion internal/formatter/network_allow_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import (
)

const (
defaultNalListing = "table {{.Name}}\t{{.Desc}}\t{{.AllowedList}}\t{{.Clusters}}"
defaultNalListing = "table {{.Name}}\t{{.Desc}}\t{{.AllowedList}}\t{{.Clusters}}\t{{.ApiKeys}}"
networkAllowListHeader = "Allow List"
)

Expand Down Expand Up @@ -67,6 +67,7 @@ func NewNetworkAllowListContext() *NetworkAllowListContext {
"Clusters": clustersHeader,
"Desc": descriptionHeader,
"Name": nameHeader,
"ApiKeys": "API keys",
}
return &nalCtx
}
Expand All @@ -89,6 +90,17 @@ func (c *NetworkAllowListContext) Clusters() string {
}
return strings.Join(clusterNameList, ",")
}

func (c *NetworkAllowListContext) ApiKeys() string {
var apiKeyNames []string
for _, apiKey := range c.c.GetInfo().ApiKeyList {
if apiKey.Status != nil && *apiKey.Status == ybmclient.APIKEYSTATUSENUM_ACTIVE {
apiKeyNames = append(apiKeyNames, apiKey.Name)
}
}
return strings.Join(apiKeyNames, ",")
}

func (c *NetworkAllowListContext) MarshalJSON() ([]byte, error) {
return json.Marshal(c.c)
}

0 comments on commit 9d9ba9a

Please sign in to comment.