Skip to content

Commit df7c3d1

Browse files
committed
Add API authentication commands for SDK/Terraform integration
Introduce 'stackit auth api' subcommand group to enable Terraform Provider and SDK to use CLI user credentials instead of requiring service accounts for local development. New commands: - stackit auth api login: Authenticate for SDK/Terraform usage - stackit auth api logout: Remove API credentials - stackit auth api get-access-token: Get valid access token (with auto-refresh) - stackit auth api status: Show API authentication status API auth uses separate storage context (StorageContextAPI) from CLI auth, allowing concurrent authentication with different accounts.
1 parent c9e8892 commit df7c3d1

File tree

8 files changed

+313
-1
lines changed

8 files changed

+313
-1
lines changed
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package getaccesstoken
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
7+
"github.com/spf13/cobra"
8+
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
9+
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
10+
"github.com/stackitcloud/stackit-cli/internal/pkg/auth"
11+
cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors"
12+
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
13+
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
14+
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
15+
)
16+
17+
type inputModel struct {
18+
*globalflags.GlobalFlagModel
19+
}
20+
21+
func NewCmd(params *params.CmdParams) *cobra.Command {
22+
cmd := &cobra.Command{
23+
Use: "get-access-token",
24+
Short: "Prints a short-lived access token for the STACKIT Terraform Provider and SDK",
25+
Long: "Prints a short-lived access token for the STACKIT Terraform Provider and SDK which can be used e.g. for API calls.",
26+
Args: args.NoArgs,
27+
Example: examples.Build(
28+
examples.NewExample(
29+
`Print a short-lived access token for the STACKIT Terraform Provider and SDK`,
30+
"$ stackit auth provider get-access-token"),
31+
),
32+
RunE: func(cmd *cobra.Command, args []string) error {
33+
model, err := parseInput(params.Printer, cmd, args)
34+
if err != nil {
35+
return err
36+
}
37+
38+
userSessionExpired, err := auth.UserSessionExpiredWithContext(auth.StorageContextAPI)
39+
if err != nil {
40+
return err
41+
}
42+
if userSessionExpired {
43+
return &cliErr.SessionExpiredError{}
44+
}
45+
46+
accessToken, err := auth.GetValidAccessTokenWithContext(params.Printer, auth.StorageContextAPI)
47+
if err != nil {
48+
params.Printer.Debug(print.ErrorLevel, "get valid access token: %v", err)
49+
return &cliErr.SessionExpiredError{}
50+
}
51+
52+
switch model.OutputFormat {
53+
case print.JSONOutputFormat:
54+
details, err := json.MarshalIndent(map[string]string{
55+
"access_token": accessToken,
56+
}, "", " ")
57+
if err != nil {
58+
return fmt.Errorf("marshal access token: %w", err)
59+
}
60+
params.Printer.Outputln(string(details))
61+
62+
return nil
63+
default:
64+
params.Printer.Outputln(accessToken)
65+
66+
return nil
67+
}
68+
},
69+
}
70+
71+
// hide project id flag from help command because it could mislead users
72+
cmd.SetHelpFunc(func(command *cobra.Command, strings []string) {
73+
_ = command.Flags().MarkHidden(globalflags.ProjectIdFlag) // nolint:errcheck // there's no chance to handle the error here
74+
command.Parent().HelpFunc()(command, strings)
75+
})
76+
77+
return cmd
78+
}
79+
80+
func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) {
81+
globalFlags := globalflags.Parse(p, cmd)
82+
83+
model := inputModel{
84+
GlobalFlagModel: globalFlags,
85+
}
86+
87+
p.DebugInputModel(model)
88+
return &model, nil
89+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package login
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/spf13/cobra"
7+
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
8+
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
9+
"github.com/stackitcloud/stackit-cli/internal/pkg/auth"
10+
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
11+
)
12+
13+
func NewCmd(params *params.CmdParams) *cobra.Command {
14+
cmd := &cobra.Command{
15+
Use: "login",
16+
Short: "Logs in for the STACKIT Terraform Provider and SDK",
17+
Long: fmt.Sprintf("%s\n%s\n%s",
18+
"Logs in for the STACKIT Terraform Provider and SDK using a user account.",
19+
"The authentication is done via a web-based authorization flow, where the command will open a browser window in which you can login to your STACKIT account.",
20+
"The credentials are stored separately from the CLI authentication and will be used by the STACKIT Terraform Provider and SDK."),
21+
Args: args.NoArgs,
22+
Example: examples.Build(
23+
examples.NewExample(
24+
`Login for the STACKIT Terraform Provider and SDK. This command will open a browser window where you can login to your STACKIT account`,
25+
"$ stackit auth provider login"),
26+
),
27+
RunE: func(_ *cobra.Command, _ []string) error {
28+
err := auth.AuthorizeUser(params.Printer, auth.StorageContextAPI, false)
29+
if err != nil {
30+
return fmt.Errorf("authorization failed: %w", err)
31+
}
32+
33+
params.Printer.Outputln("Successfully logged in for STACKIT Terraform Provider and SDK.\n")
34+
35+
return nil
36+
},
37+
}
38+
return cmd
39+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package logout
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/spf13/cobra"
7+
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
8+
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
9+
"github.com/stackitcloud/stackit-cli/internal/pkg/auth"
10+
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
11+
)
12+
13+
func NewCmd(params *params.CmdParams) *cobra.Command {
14+
cmd := &cobra.Command{
15+
Use: "logout",
16+
Short: "Logs out from the STACKIT Terraform Provider and SDK",
17+
Long: "Logs out from the STACKIT Terraform Provider and SDK. This does not affect CLI authentication.",
18+
Args: args.NoArgs,
19+
Example: examples.Build(
20+
examples.NewExample(
21+
`Log out from the STACKIT Terraform Provider and SDK`,
22+
"$ stackit auth provider logout"),
23+
),
24+
RunE: func(_ *cobra.Command, _ []string) error {
25+
err := auth.LogoutUserWithContext(auth.StorageContextAPI)
26+
if err != nil {
27+
return fmt.Errorf("log out failed: %w", err)
28+
}
29+
30+
params.Printer.Info("Successfully logged out from STACKIT Terraform Provider and SDK.\n")
31+
return nil
32+
},
33+
}
34+
return cmd
35+
}

internal/cmd/auth/api/provider.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package api
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
getaccesstoken "github.com/stackitcloud/stackit-cli/internal/cmd/auth/api/get-access-token"
6+
"github.com/stackitcloud/stackit-cli/internal/cmd/auth/api/login"
7+
"github.com/stackitcloud/stackit-cli/internal/cmd/auth/api/logout"
8+
"github.com/stackitcloud/stackit-cli/internal/cmd/auth/api/status"
9+
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
10+
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
11+
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
12+
)
13+
14+
func NewCmd(params *params.CmdParams) *cobra.Command {
15+
cmd := &cobra.Command{
16+
Use: "api",
17+
Short: "Manages authentication for the STACKIT Terraform Provider and SDK",
18+
Long: `Manages authentication for the STACKIT Terraform Provider and SDK.
19+
20+
These commands allow you to authenticate with your personal STACKIT account
21+
and share the credentials with the STACKIT Terraform Provider and SDK.
22+
This provides an alternative to using service accounts for local development.`,
23+
Args: args.NoArgs,
24+
Run: utils.CmdHelp,
25+
}
26+
addSubcommands(cmd, params)
27+
return cmd
28+
}
29+
30+
func addSubcommands(cmd *cobra.Command, params *params.CmdParams) {
31+
cmd.AddCommand(login.NewCmd(params))
32+
cmd.AddCommand(logout.NewCmd(params))
33+
cmd.AddCommand(getaccesstoken.NewCmd(params))
34+
cmd.AddCommand(status.NewCmd(params))
35+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package status
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
7+
"github.com/spf13/cobra"
8+
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
9+
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
10+
"github.com/stackitcloud/stackit-cli/internal/pkg/auth"
11+
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
12+
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
13+
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
14+
)
15+
16+
type inputModel struct {
17+
*globalflags.GlobalFlagModel
18+
}
19+
20+
type statusOutput struct {
21+
Authenticated bool `json:"authenticated"`
22+
Email string `json:"email,omitempty"`
23+
AuthFlow string `json:"auth_flow,omitempty"`
24+
}
25+
26+
func NewCmd(params *params.CmdParams) *cobra.Command {
27+
cmd := &cobra.Command{
28+
Use: "status",
29+
Short: "Shows authentication status for the STACKIT Terraform Provider and SDK",
30+
Long: "Shows authentication status for the STACKIT Terraform Provider and SDK, including whether you are authenticated and with which account.",
31+
Args: args.NoArgs,
32+
Example: examples.Build(
33+
examples.NewExample(
34+
`Show authentication status for the STACKIT Terraform Provider and SDK`,
35+
"$ stackit auth api status"),
36+
),
37+
RunE: func(cmd *cobra.Command, args []string) error {
38+
model, err := parseInput(params.Printer, cmd, args)
39+
if err != nil {
40+
return err
41+
}
42+
43+
// Check if access token exists (primary credential check)
44+
accessToken, err := auth.GetAuthFieldWithContext(auth.StorageContextAPI, auth.ACCESS_TOKEN)
45+
if err != nil || accessToken == "" {
46+
// Not authenticated
47+
return outputStatus(params.Printer, model, statusOutput{
48+
Authenticated: false,
49+
})
50+
}
51+
52+
// Get optional fields for display
53+
flow, _ := auth.GetAuthFlowWithContext(auth.StorageContextAPI)
54+
email, err := auth.GetAuthFieldWithContext(auth.StorageContextAPI, auth.USER_EMAIL)
55+
if err != nil {
56+
email = ""
57+
}
58+
59+
return outputStatus(params.Printer, model, statusOutput{
60+
Authenticated: true,
61+
Email: email,
62+
AuthFlow: string(flow),
63+
})
64+
},
65+
}
66+
67+
// hide project id flag from help command because it could mislead users
68+
cmd.SetHelpFunc(func(command *cobra.Command, strings []string) {
69+
_ = command.Flags().MarkHidden(globalflags.ProjectIdFlag) // nolint:errcheck // there's no chance to handle the error here
70+
command.Parent().HelpFunc()(command, strings)
71+
})
72+
73+
return cmd
74+
}
75+
76+
func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) {
77+
globalFlags := globalflags.Parse(p, cmd)
78+
79+
model := inputModel{
80+
GlobalFlagModel: globalFlags,
81+
}
82+
83+
p.DebugInputModel(model)
84+
return &model, nil
85+
}
86+
87+
func outputStatus(p *print.Printer, model *inputModel, status statusOutput) error {
88+
switch model.OutputFormat {
89+
case print.JSONOutputFormat:
90+
details, err := json.MarshalIndent(status, "", " ")
91+
if err != nil {
92+
return fmt.Errorf("marshal status: %w", err)
93+
}
94+
p.Outputln(string(details))
95+
return nil
96+
default:
97+
if status.Authenticated {
98+
p.Outputln("API Authentication Status: Authenticated")
99+
if status.Email != "" {
100+
p.Outputf("Email: %s\n", status.Email)
101+
}
102+
p.Outputf("Auth Flow: %s\n", status.AuthFlow)
103+
} else {
104+
p.Outputln("API Authentication Status: Not authenticated")
105+
p.Outputln("\nTo authenticate, run: stackit auth api login")
106+
}
107+
return nil
108+
}
109+
}

internal/cmd/auth/auth.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package auth
22

33
import (
44
activateserviceaccount "github.com/stackitcloud/stackit-cli/internal/cmd/auth/activate-service-account"
5+
"github.com/stackitcloud/stackit-cli/internal/cmd/auth/api"
56
getaccesstoken "github.com/stackitcloud/stackit-cli/internal/cmd/auth/get-access-token"
67
"github.com/stackitcloud/stackit-cli/internal/cmd/auth/login"
78
"github.com/stackitcloud/stackit-cli/internal/cmd/auth/logout"
@@ -29,4 +30,5 @@ func addSubcommands(cmd *cobra.Command, params *types.CmdParams) {
2930
cmd.AddCommand(logout.NewCmd(params))
3031
cmd.AddCommand(activateserviceaccount.NewCmd(params))
3132
cmd.AddCommand(getaccesstoken.NewCmd(params))
33+
cmd.AddCommand(api.NewCmd(params))
3234
}

internal/cmd/auth/login/login.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command {
2626
"$ stackit auth login"),
2727
),
2828
RunE: func(_ *cobra.Command, _ []string) error {
29-
err := auth.AuthorizeUser(params.Printer, false)
29+
err := auth.AuthorizeUser(params.Printer, auth.StorageContextCLI, false)
3030
if err != nil {
3131
return fmt.Errorf("authorization failed: %w", err)
3232
}

internal/pkg/auth/service_account.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ var _ http.RoundTripper = &keyFlowWithStorage{}
3737
// It returns the email associated with the service account
3838
// If disableWriting is set to true the credentials are not stored on disk (keyring, file).
3939
func AuthenticateServiceAccount(p *print.Printer, rt http.RoundTripper, disableWriting bool) (email, accessToken string, err error) {
40+
// Set the storage printer so debug messages use the correct verbosity
41+
SetStoragePrinter(p)
42+
4043
authFields := make(map[authFieldKey]string)
4144
var authFlowType AuthFlow
4245
switch flow := rt.(type) {

0 commit comments

Comments
 (0)