Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/user-guide/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ Create a new context profile.
| `--tls-skip-verify` | Skip TLS certificate verification |
| `--ca-cert` | Path to CA certificate file |

If `--token` is omitted and `A7_TOKEN` is set, `context create` uses that
environment token for validation and persists it into the new context. It does
not copy the token from the previously active context.

The first context you create is automatically set as the current context.

```bash
Expand Down
7 changes: 5 additions & 2 deletions docs/user-guide/route.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,19 +122,22 @@ a7 route delete 12345 -g default --force
### `a7 route export`

Exports routes from a gateway group to a file or stdout.
API7 EE requires `--service-id` for route export because routes are scoped by
service.

| Flag | Short | Default | Description |
|------|-------|---------|-------------|
| `--gateway-group` | `-g` | | Target gateway group name (required) |
| `--service-id` | | | Service ID whose routes should be exported (required by API7 EE) |
| `--label` | | | Filter routes to export by label |
| `--output` | `-o` | `yaml` | Output format (json, yaml) |
| `--file` | `-f` | | Path to save the exported configuration |

**Examples:**

Export all routes to a YAML file:
Export routes for a service to a YAML file:
```bash
a7 route export -g default -f all-routes.yaml
a7 route export -g default --service-id example-service -f routes.yaml
```

## Configuration Reference
Expand Down
20 changes: 17 additions & 3 deletions docs/user-guide/secret.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,15 @@ a7 secret get vault/prod-vault -g default
### `a7 secret create`

Creates a new secret manager from a JSON or YAML file using a compound ID.
Flag mode is also supported for Vault-style provider configuration. Use
`--provider-token` for the secret backend token; the global `--token` flag is
reserved for the API7 EE API token.

| Flag | Short | Default | Description |
|------|-------|---------|-------------|
| `--gateway-group` | `-g` | | Target gateway group name (required) |
| `--file` | `-f` | | Path to the secret configuration file (required) |
| `--file` | `-f` | | Path to the secret configuration file (required unless using flag mode) |
| `--provider-token` | | | Secret provider token for flag mode |
| `--output` | `-o` | `yaml` | Output format (json, yaml) |

**Examples:**
Expand All @@ -68,14 +72,24 @@ Create an AWS secret manager:
a7 secret create aws/my-aws -g default -f aws-config.yaml
```

Create a Vault secret manager with flags:
```bash
a7 secret create vault/my-vault -g default \
--uri https://vault.example.com \
--prefix apisix/prod \
--provider-token hvs.CAES...
```

### `a7 secret update`

Updates an existing secret manager by compound ID.
Updates an existing secret manager by compound ID. As with create, use
`--provider-token` for the secret backend token in flag mode.

| Flag | Short | Default | Description |
|------|-------|---------|-------------|
| `--gateway-group` | `-g` | | Target gateway group name (required) |
| `--file` | `-f` | | Path to the secret configuration file (required) |
| `--file` | `-f` | | Path to the secret configuration file (required unless using flag mode) |
| `--provider-token` | | | Secret provider token for flag mode |
| `--output` | `-o` | `yaml` | Output format (json, yaml) |
Comment on lines +85 to 93

**Examples:**
Expand Down
16 changes: 9 additions & 7 deletions docs/user-guide/stream-route.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,20 @@ The `a7 stream-route` command allows you to manage API7 Enterprise Edition (API7

### `a7 stream-route list`

Lists all stream routes in the specified gateway group.
Lists stream routes for a service in the specified gateway group. API7 EE
requires `--service-id` for stream-route list requests.

| Flag | Short | Default | Description |
|------|-------|---------|-------------|
| `--gateway-group` | `-g` | | Target gateway group name (required) |
| `--page` | | `1` | Page number for pagination |
| `--page-size` | | `20` | Number of items per page |
| `--service-id` | | | Service ID whose stream routes should be listed (required by API7 EE) |
| `--output` | `-o` | `table` | Output format (table, json, yaml) |

**Examples:**

List all stream routes in the "default" gateway group:
List stream routes for a service in the "default" gateway group:
```bash
a7 stream-route list -g default
a7 stream-route list -g default --service-id example-service
```

### `a7 stream-route get <id>`
Expand Down Expand Up @@ -109,18 +109,20 @@ a7 stream-route delete 1 -g default --force
### `a7 stream-route export`

Exports stream routes from a gateway group to a file or stdout.
API7 EE requires `--service-id` for stream-route export requests.

| Flag | Short | Default | Description |
|------|-------|---------|-------------|
| `--gateway-group` | `-g` | | Target gateway group name (required) |
| `--service-id` | | | Service ID whose stream routes should be exported (required by API7 EE) |
| `--output` | `-o` | `yaml` | Output format (json, yaml) |
| `--file` | `-f` | | Path to save the exported configuration |

**Examples:**

Export all stream routes to a YAML file:
Export stream routes for a service to a YAML file:
```bash
a7 stream-route export -g default -f all-stream-routes.yaml
a7 stream-route export -g default --service-id example-service -f stream-routes.yaml
```

## Configuration Reference
Expand Down
2 changes: 1 addition & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ func (c *FileConfig) Save() error {
defer c.mu.RUnlock()

dir := filepath.Dir(c.path)
if err := os.MkdirAll(dir, 0o755); err != nil {
if err := os.MkdirAll(dir, 0o700); err != nil {
return fmt.Errorf("failed to create config directory: %w", err)
}

Expand Down
19 changes: 19 additions & 0 deletions pkg/api/types_secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,22 @@ type Secret struct {
Token string `json:"token,omitempty" yaml:"token,omitempty"`
Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
}

const RedactedSecretToken = "<redacted>"

// RedactSecret returns a copy safe for CLI output.
func RedactSecret(secret Secret) Secret {
if secret.Token != "" {
secret.Token = RedactedSecretToken
}
return secret
}

// RedactSecrets returns copies safe for CLI output.
func RedactSecrets(secrets []Secret) []Secret {
redacted := make([]Secret, len(secrets))
for i, secret := range secrets {
redacted[i] = RedactSecret(secret)
}
return redacted
}
19 changes: 19 additions & 0 deletions pkg/api/types_ssl.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,22 @@ type SSL struct {
Status int `json:"status,omitempty" yaml:"status,omitempty"`
Type string `json:"type,omitempty" yaml:"type,omitempty"`
}

const RedactedSSLKey = "<redacted>"

// RedactSSL returns a copy safe for CLI output.
func RedactSSL(ssl SSL) SSL {
if ssl.Key != "" {
ssl.Key = RedactedSSLKey
}
return ssl
}

// RedactSSLs returns copies safe for CLI output.
func RedactSSLs(ssls []SSL) []SSL {
redacted := make([]SSL, len(ssls))
for i, ssl := range ssls {
redacted[i] = RedactSSL(ssl)
}
return redacted
}
Comment on lines +14 to +31
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

SSL struct must implement MarshalJSON()/String() redaction per coding guidelines — standalone helpers are opt-in only.

SSL.Key is a secret-bearing field. The standalone RedactSSL/RedactSSLs helpers require every output site to explicitly opt in; any direct json.Marshal(ssl) call (e.g., in a future command, a logger, or a fmt.Sprintf("%+v", ...)) silently leaks the private key.

The coding guidelines mandate that the struct itself enforce redaction via String(), MarshalJSON(), and MarshalLogObject() methods. The typical solution where the same struct is used for API writes (which need the real key) is to split into separate types — e.g. an SSLRequest for API writes and an SSL/SSLResponse for read/display — so that MarshalJSON() on the display type always redacts.

As per coding guidelines: "All secret-bearing structs must implement proper redaction in String(), MarshalJSON(), and MarshalLogObject() methods; verify pointer receiver vs value receiver matches."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/api/types_ssl.go` around lines 14 - 31, The SSL.Key field is secret and
standalone helpers (RedactSSL/RedactSSLs) are insufficient; modify the types so
display/JSON/logging always redact: introduce a separate API-write type (e.g.,
SSLRequest) that holds the real Key, keep or create a display type (e.g., SSL or
SSLResponse) used for CLI/logs and implement String(), MarshalJSON(), and
MarshalLogObject() on that display type to replace Key with RedactedSSLKey;
update code paths to use SSLRequest for inbound API writes and the redacting
display type for any output/serialization, ensuring method receiver choices
match the guidelines (pointer vs value) and removing reliance on callers to call
RedactSSL/RedactSSLs.

4 changes: 4 additions & 0 deletions pkg/cmd/context/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"net/http"
"os"
"time"

"github.com/spf13/cobra"
Expand Down Expand Up @@ -109,6 +110,9 @@ func createRun(opts *Options, f *cmd.Factory) error {
TLSSkipVerify: opts.TLSSkipVerify,
CACert: opts.CACert,
}
if ctx.Token == "" {
ctx.Token = os.Getenv("A7_TOKEN")
}

// Validate context before saving (unless --skip-validation is set)
if !opts.SkipValidation {
Expand Down
67 changes: 67 additions & 0 deletions pkg/cmd/context/create/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,73 @@ func TestCreateRun_SkipValidation(t *testing.T) {
assert.Equal(t, "fake-token", saved.Token)
}

func TestCreateRun_UsesA7TokenEnvWhenTokenFlagOmitted(t *testing.T) {
cfgPath := fmt.Sprintf("%s/config.yaml", t.TempDir())
cfg := config.NewFileConfigWithPath(cfgPath)
t.Setenv("A7_TOKEN", "env-token")

opts := &Options{
Config: func() (config.Config, error) {
return cfg, nil
},
Name: "env-ctx",
Server: "https://127.0.0.1:1",
SkipValidation: true,
}

ios, _, _, _ := iostreams.Test()
f := &cmd.Factory{
IOStreams: ios,
Config: func() (config.Config, error) {
return cfg, nil
},
}

err := createRun(opts, f)
require.NoError(t, err)

saved, err := cfg.GetContext("env-ctx")
require.NoError(t, err)
assert.Equal(t, "env-token", saved.Token)
}

func TestCreateRun_DoesNotReuseCurrentContextTokenWhenTokenFlagOmitted(t *testing.T) {
cfgPath := fmt.Sprintf("%s/config.yaml", t.TempDir())
cfg := config.NewFileConfigWithPath(cfgPath)
require.NoError(t, cfg.AddContext(config.Context{
Name: "current",
Server: "https://current.example.com",
Token: "current-token",
}))
require.NoError(t, cfg.SetCurrentContext("current"))
require.NoError(t, cfg.Save())
t.Setenv("A7_TOKEN", "")

opts := &Options{
Config: func() (config.Config, error) {
return cfg, nil
},
Name: "new-ctx",
Server: "https://127.0.0.1:1",
SkipValidation: true,
}

ios, _, _, _ := iostreams.Test()
f := &cmd.Factory{
IOStreams: ios,
Config: func() (config.Config, error) {
return cfg, nil
},
}

err := createRun(opts, f)
require.NoError(t, err)

saved, err := cfg.GetContext("new-ctx")
require.NoError(t, err)
assert.Empty(t, saved.Token)
}

func TestCreateRun_ValidationFails(t *testing.T) {
cfgPath := fmt.Sprintf("%s/config.yaml", t.TempDir())
cfg := config.NewFileConfigWithPath(cfgPath)
Expand Down
31 changes: 27 additions & 4 deletions pkg/cmd/credential/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,18 @@ func actionRun(opts *Options) error {
}

if opts.ID != "" {
payload["id"] = opts.ID
payload["name"] = opts.ID
delete(payload, "id")
} else if _, ok := payload["name"]; ok {
delete(payload, "id")
} else if id, ok := payload["id"]; ok {
payload["name"] = id
delete(payload, "id")
}

name, hasName, err := credentialNameFromPayload(payload)
if err != nil {
return err
}

httpClient, err := opts.Client()
Expand All @@ -91,8 +102,8 @@ func actionRun(opts *Options) error {
path := "/apisix/admin/consumers/" + opts.Consumer + "/credentials?gateway_group_id=" + ggID
client := api.NewClient(httpClient, cfg.BaseURL())
var body []byte
if id, ok := payload["id"]; ok {
body, err = client.Put(fmt.Sprintf("/apisix/admin/consumers/%s/credentials/%v?gateway_group_id=%s", opts.Consumer, id, ggID), payload)
if hasName {
body, err = client.Put(fmt.Sprintf("/apisix/admin/consumers/%s/credentials/%s?gateway_group_id=%s", opts.Consumer, name, ggID), payload)
} else {
Comment on lines 82 to 107
body, err = client.Post(path, payload)
}
Expand Down Expand Up @@ -128,7 +139,7 @@ func actionRun(opts *Options) error {
labels[parts[0]] = parts[1]
}

bodyReq := api.Credential{Desc: opts.Desc}
bodyReq := api.Credential{Name: opts.ID, Desc: opts.Desc}
if len(pl) > 0 {
bodyReq.Plugins = pl
}
Expand All @@ -154,3 +165,15 @@ func actionRun(opts *Options) error {
}
return cmdutil.NewExporter(format, opts.IO.Out).Write(created)
}

func credentialNameFromPayload(payload map[string]interface{}) (string, bool, error) {
rawName, ok := payload["name"]
if !ok {
return "", false, nil
}
name, ok := rawName.(string)
if !ok || strings.TrimSpace(name) == "" {
return "", false, fmt.Errorf("credential name must be a non-empty string")
}
return name, true, nil
}
Loading
Loading