Skip to content
Open
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
46 changes: 20 additions & 26 deletions internal/cli/config.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package cli

import (
"errors"
"context"
"fmt"
"os"

"github.com/spf13/cobra"

"github.com/sufforest/drift/internal/credentials"
"github.com/sufforest/drift/internal/workspace"
)

Expand All @@ -26,53 +25,48 @@ func configSetParentCmd() *cobra.Command {
Short: "Deprecated alias for `drift parent set` (kept for script compatibility)",
Hidden: true,
Long: `Deprecated: prefer 'drift parent set'. This command remains for
scripts that pre-date the 'drift parent' namespace but does NOT run the
live HEAD verification step. New code should call 'drift parent set'.`,
scripts that pre-date the 'drift parent' namespace and now reuses the
same verified credential-replacement flow as 'drift parent set'.`,
RunE: runConfigSetParent,
}
cmd.Flags().String("access-key", "", "New access key ID (defaults to $DRIFT_ACCESS_KEY_ID)")
cmd.Flags().String("secret-key", "", "New secret access key (defaults to $DRIFT_SECRET_ACCESS_KEY)")
return cmd
}

func runConfigSetParent(cmd *cobra.Command, _ []string) error {
dir, err := stateDir(cmd)
if err != nil {
return err
ctx := cmd.Context()
if ctx == nil {
ctx = context.Background()
}
state, err := workspace.NewState(dir)
ws, err := loadWorkspace(ctx, cmd)
if err != nil {
return err
}
existing, err := state.LoadParent()
if err != nil {
return fmt.Errorf("load existing parent cred: %w", err)
}

ak, _ := cmd.Flags().GetString("access-key")
if ak == "" {
ak = os.Getenv("DRIFT_ACCESS_KEY_ID")
}
sk, _ := cmd.Flags().GetString("secret-key")
sk := os.Getenv("DRIFT_SECRET_ACCESS_KEY")
if sk == "" {
sk = os.Getenv("DRIFT_SECRET_ACCESS_KEY")
}
if ak == "" || sk == "" {
return errors.New("set-parent: missing access-key or secret-key (pass flags or export DRIFT_ACCESS_KEY_ID + DRIFT_SECRET_ACCESS_KEY)")
prompted, perr := promptPassphrase("New secret access key (input hidden): ")
if perr != nil {
return perr
}
sk = prompted
}

updated := &credentials.Parent{
Provider: existing.Provider,
res, err := ws.ParentSet(ctx, workspace.ParentSetOptions{
AccessKeyID: ak,
SecretAccessKey: sk,
}
if err := state.SaveParent(updated); err != nil {
return fmt.Errorf("save parent: %w", err)
SkipVerify: false,
})
if err != nil {
return err
}
fmt.Fprintf(cmd.OutOrStdout(),
"✓ Parent credential updated.\n"+
" Provider: %s (unchanged)\n"+
" Provider: %s\n"+
" Access key: %s…\n",
updated.Provider, abbrev(ak, 6))
res.Provider, abbrev(ak, 6))
return nil
}
2 changes: 1 addition & 1 deletion internal/cli/grant.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ ssh config (no flags are forwarded — use ~/.ssh/config for identity/port).`,
}
cmd.Flags().StringSlice("scope", nil, "Vols to grant (comma-separated)")
cmd.Flags().String("mode", "rw", "Access mode: rw or ro")
cmd.Flags().Duration("expires", 24*time.Hour, "Token TTL (≤24h recommended)")
cmd.Flags().Duration("expires", 24*time.Hour, "Token TTL (max 24h)")
cmd.Flags().Bool("token-only", false, "Print ONLY the token on stdout (status to stderr)")
cmd.Flags().String("out", "", "Write token to this file at chmod 0600 instead of stdout")
cmd.Flags().String("ssh", "", "Deliver via SSH: runs `drift open --stdin --background` on user@host")
Expand Down
16 changes: 9 additions & 7 deletions internal/cli/parent.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,8 @@ func parentSetCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "set",
Short: "Replace the parent S3 credential with a new AK/SK pair",
Long: `Reads the new AK/SK from --access-key + $DRIFT_SECRET_ACCESS_KEY (or
--secret-key, though passing secrets as CLI flags is visible in process
listings — env var is recommended) and overwrites the locally-stored
Long: `Reads the new AK/SK from --access-key + $DRIFT_SECRET_ACCESS_KEY (or an
interactive no-echo prompt if env var is unset) and overwrites the locally-stored
parent credential.

By default, the new credential is verified by issuing a HEAD against
Expand All @@ -50,7 +49,6 @@ running set themselves.`,
RunE: runParentSet,
}
cmd.Flags().String("access-key", "", "New access key ID (or $DRIFT_ACCESS_KEY_ID)")
cmd.Flags().String("secret-key", "", "New secret access key (PREFER $DRIFT_SECRET_ACCESS_KEY to keep secrets out of argv)")
cmd.Flags().String("provider", "", "Override the provider id (default: keep existing — typically 'r2')")
cmd.Flags().Bool("skip-verify", false, "Skip the live HEAD probe (dangerous: a wrong secret will silently brick this device's R2 access)")
return cmd
Expand All @@ -67,12 +65,16 @@ func runParentSet(cmd *cobra.Command, _ []string) error {
if ak == "" {
ak = os.Getenv("DRIFT_ACCESS_KEY_ID")
}
sk, _ := cmd.Flags().GetString("secret-key")
sk := os.Getenv("DRIFT_SECRET_ACCESS_KEY")
if sk == "" {
sk = os.Getenv("DRIFT_SECRET_ACCESS_KEY")
prompted, perr := promptPassphrase("New secret access key (input hidden): ")
if perr != nil {
return perr
}
sk = prompted
}
if ak == "" || sk == "" {
return errors.New("drift parent set: missing access-key or secret-key (pass flags or export DRIFT_ACCESS_KEY_ID + DRIFT_SECRET_ACCESS_KEY)")
return errors.New("drift parent set: missing access-key or secret-key (pass --access-key / DRIFT_ACCESS_KEY_ID and DRIFT_SECRET_ACCESS_KEY or use interactive secret prompt)")
}
provider, _ := cmd.Flags().GetString("provider")
skipVerify, _ := cmd.Flags().GetBool("skip-verify")
Expand Down
9 changes: 8 additions & 1 deletion internal/token/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,13 @@ type IssueRequest struct {

Scope []string // compartment names to grant
Mode string // TokenModeRW / TokenModeRO
TTL time.Duration // <= 24h recommended
TTL time.Duration // max TokenMaxTTL
}

// TokenMaxTTL caps issued bearer-token lifetime. Short-lived creds reduce
// exposure if a token string leaks from chat/log/history.
const TokenMaxTTL = 24 * time.Hour

// IssueResult is what Issue returns to the caller. Both scoped creds are
// included so the CLI / workspace layer can show their expiry, etc.
type IssueResult struct {
Expand Down Expand Up @@ -220,6 +224,9 @@ func validateIssue(req IssueRequest) error {
if req.TTL <= 0 {
return errors.New("token: TTL must be > 0")
}
if req.TTL > TokenMaxTTL {
return fmt.Errorf("token: TTL %s exceeds max %s", req.TTL, TokenMaxTTL)
}
for _, name := range req.Scope {
if err := domain.ValidCompartmentName(name); err != nil {
return err
Expand Down
1 change: 1 addition & 0 deletions internal/token/token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ func TestIssue_validationFailures(t *testing.T) {
"empty cprk": func(r *IssueRequest) { r.CPRK = nil },
"empty wid": func(r *IssueRequest) { r.WorkspaceID = "" },
"zero ttl": func(r *IssueRequest) { r.TTL = 0 },
"ttl too long": func(r *IssueRequest) { r.TTL = TokenMaxTTL + time.Second },
"bad mode": func(r *IssueRequest) { r.Mode = "execute" },
}
for name, mutate := range cases {
Expand Down
Loading