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
10 changes: 9 additions & 1 deletion internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import (
"golang.org/x/term"
)

const configFilePath = "config.yaml"

// Global flags that apply to all commands
type Globals struct {
// Configuration handling
Expand Down Expand Up @@ -109,7 +111,7 @@ func Parse(args []string, amtCommand amt.Interface) (*kong.Context, *CLI, error)
kong.UsageOnError(),
kong.DefaultEnvars("RPC"),
kong.ConfigureHelp(helpOpts),
kong.Configuration(kongyaml.Loader, "config.yaml"),
kong.Configuration(kongyaml.Loader, configFilePath),
kong.BindToProvider(func() amt.Interface { return amtCommand }),
}

Expand All @@ -127,6 +129,12 @@ func Parse(args []string, amtCommand amt.Interface) (*kong.Context, *CLI, error)
}

ctx, perr := parser.Parse(parseArgs)

// Log config file presence after parsing (logging is configured by AfterApply at this point)
if _, statErr := os.Stat(configFilePath); statErr == nil {
log.Infof("Using configuration file: %s (flag values may originate from this file)", configFilePath)
}

if perr == nil {
return ctx, &cli, nil
}
Expand Down
2 changes: 1 addition & 1 deletion internal/commands/activate/activate.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ func (cmd *ActivateCmd) runHttpProfileFullflow(ctx *commands.Context) error {

cfg, err := fetcher.FetchProfile()
if err != nil {
return fmt.Errorf("failed to fetch profile: %w", err)
return err
}

// Resolve AMT/MEBx/MPS passwords — generate random ones when the profile requests it.
Expand Down
35 changes: 31 additions & 4 deletions internal/commands/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,41 @@ import (
"github.com/sirupsen/logrus"
)

// ServerAuthFlags provides common auth options for server communications
// Either AuthToken (Bearer) OR both AuthUsername and AuthPassword (Basic) should be supplied.
// ServerAuthFlags provides common auth options for server communications.
// When AuthEndpoint is set, either AuthToken (Bearer) OR both AuthUsername
// and AuthPassword (Basic) must be supplied.
type ServerAuthFlags struct {
AuthToken string `help:"Bearer token for server authentication" name:"auth-token" env:"AUTH_TOKEN"`
AuthUsername string `help:"Username for basic auth (used when no token)" name:"auth-username" env:"AUTH_USERNAME"`
AuthPassword string `help:"Password for basic auth (used when no token)" name:"auth-password" env:"AUTH_PASSWORD"`
// Optional endpoint for exchanging credentials for a token (primarily used when fetching HTTP profiles)
AuthEndpoint string `help:"The endpoint to call to fetch a token. Assumes the same host as the profile URL unless an absolute URL is provided; defaults to the Console path /api/v1/authorize." name:"auth-endpoint" default:"/api/v1/authorize"`
AuthEndpoint string `help:"Token exchange endpoint. Requires --auth-token or --auth-username/--auth-password. Resolved relative to the profile URL host unless absolute." name:"auth-endpoint" env:"AUTH_ENDPOINT"`
}

// Validate implements kong.Validatable.
// - auth-username and auth-password must always be provided together.
// - When auth-endpoint is set, either auth-token or (auth-username + auth-password) is required.
func (a *ServerAuthFlags) Validate() error {
if (a.AuthUsername != "") != (a.AuthPassword != "") {
if a.AuthUsername != "" {
return fmt.Errorf("--auth-username requires --auth-password")
}

return fmt.Errorf("--auth-password requires --auth-username")
}

if a.AuthEndpoint == "" {
return nil
}

if a.AuthToken != "" {
Comment thread
rsdmike marked this conversation as resolved.
return nil
}

if a.AuthUsername != "" && a.AuthPassword != "" {
return nil
}

return fmt.Errorf("--auth-endpoint requires --auth-token or both --auth-username and --auth-password")
}

// ValidateRequired enforces that some form of auth is present when required.
Expand Down
187 changes: 187 additions & 0 deletions internal/commands/auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/*********************************************************************
* Copyright (c) Intel Corporation 2024
* SPDX-License-Identifier: Apache-2.0
**********************************************************************/

package commands

import (
"context"
"net/http"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestServerAuthFlags_Validate(t *testing.T) {
tests := []struct {
name string
flags ServerAuthFlags
wantErr string
}{
{
name: "no endpoint — no validation",
flags: ServerAuthFlags{},
},
{
name: "endpoint with token — ok",
flags: ServerAuthFlags{
AuthEndpoint: "/api/v1/authorize",
AuthToken: "tok",
},
},
{
name: "endpoint with username and password — ok",
flags: ServerAuthFlags{
AuthEndpoint: "/api/v1/authorize",
AuthUsername: "user",
AuthPassword: "pass",
},
},
{
name: "endpoint with no credentials — error",
flags: ServerAuthFlags{
AuthEndpoint: "/api/v1/authorize",
},
wantErr: "--auth-endpoint requires --auth-token or both --auth-username and --auth-password",
},
{
name: "endpoint with username only — error",
flags: ServerAuthFlags{
AuthEndpoint: "/api/v1/authorize",
AuthUsername: "user",
},
wantErr: "--auth-username requires --auth-password",
},
{
name: "endpoint with password only — error",
flags: ServerAuthFlags{
AuthEndpoint: "/api/v1/authorize",
AuthPassword: "pass",
},
wantErr: "--auth-password requires --auth-username",
},
{
name: "username only without endpoint — error",
flags: ServerAuthFlags{
AuthUsername: "user",
},
wantErr: "--auth-username requires --auth-password",
},
{
name: "password only without endpoint — error",
flags: ServerAuthFlags{
AuthPassword: "pass",
},
wantErr: "--auth-password requires --auth-username",
},
{
name: "endpoint with token and username/password — token wins, ok",
flags: ServerAuthFlags{
AuthEndpoint: "/api/v1/authorize",
AuthToken: "tok",
AuthUsername: "user",
AuthPassword: "pass",
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.flags.Validate()
if tt.wantErr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
} else {
assert.NoError(t, err)
}
})
}
}

func TestServerAuthFlags_ValidateRequired(t *testing.T) {
tests := []struct {
name string
flags ServerAuthFlags
required bool
wantErr bool
}{
{
name: "not required — always ok",
flags: ServerAuthFlags{},
required: false,
},
{
name: "required with token",
flags: ServerAuthFlags{AuthToken: "tok"},
required: true,
},
{
name: "required with username and password",
flags: ServerAuthFlags{AuthUsername: "user", AuthPassword: "pass"},
required: true,
},
{
name: "required with nothing",
flags: ServerAuthFlags{},
required: true,
wantErr: true,
},
{
name: "required with username only",
flags: ServerAuthFlags{AuthUsername: "user"},
required: true,
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.flags.ValidateRequired(tt.required)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}

func TestServerAuthFlags_ApplyToRequest(t *testing.T) {
tests := []struct {
name string
flags ServerAuthFlags
wantHeader string
}{
{
name: "token sets bearer",
flags: ServerAuthFlags{AuthToken: "my-token"},
wantHeader: "Bearer my-token",
},
{
name: "username/password sets basic",
flags: ServerAuthFlags{AuthUsername: "user", AuthPassword: "pass"},
wantHeader: "Basic dXNlcjpwYXNz",
},
{
name: "token takes precedence over basic",
flags: ServerAuthFlags{AuthToken: "tok", AuthUsername: "user", AuthPassword: "pass"},
wantHeader: "Bearer tok",
},
{
name: "no credentials — no header",
flags: ServerAuthFlags{},
wantHeader: "",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req, _ := http.NewRequestWithContext(context.Background(), "GET", "http://example.com", nil)
tt.flags.ApplyToRequest(req)

assert.Equal(t, tt.wantHeader, req.Header.Get("Authorization"))
})
}
}
6 changes: 5 additions & 1 deletion internal/profile/fetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,11 @@ func (f *ProfileFetcher) fetchData(u, token string) ([]byte, error) {
defer resp.Body.Close()

if resp.StatusCode == http.StatusUnauthorized {
return nil, fmt.Errorf("unauthorized: authentication required or token invalid")
if token == "" {
return nil, fmt.Errorf("unauthorized: no credentials provided — use --auth-token or --auth-username/--auth-password")
}

return nil, fmt.Errorf("unauthorized: server rejected the bearer token")
}

if resp.StatusCode != http.StatusOK {
Expand Down
Loading