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
2 changes: 1 addition & 1 deletion cli/commands/cluster_add.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func addCluster(ctx *Context, identityName, clusterName, address string, force b
if clusterName == "" && address == "" {
ctx.Info("Fetching available clusters from identity server...")

clusters, err := fetchAvailableClusters(ctx, mainConfig, identity)
clusters, err := fetchAvailableClusters(ctx, mainConfig, identityName, identity)
if err != nil {
return fmt.Errorf("failed to fetch available clusters: %w", err)
}
Expand Down
25 changes: 6 additions & 19 deletions cli/commands/cluster_switch.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (

"miren.dev/runtime/appconfig"
"miren.dev/runtime/clientconfig"
"miren.dev/runtime/pkg/cloudauth"
"miren.dev/runtime/pkg/ui"
)

Expand Down Expand Up @@ -133,7 +132,7 @@ func checkClusterAccess(ctx *Context, cfg *clientconfig.Config, clusterName stri
if clusterXID == "" {
// Fall back to fetching clusters and finding by name (for backwards compatibility
// with clusters added before XID was stored)
clusters, err := fetchAvailableClusters(ctx, cfg, identity)
clusters, err := fetchAvailableClusters(ctx, cfg, cluster.Identity, identity)
if err != nil {
// If we can't reach the cloud API, we'll allow the switch
// The user will get an access denied error when they try to perform actions
Expand All @@ -156,7 +155,7 @@ func checkClusterAccess(ctx *Context, cfg *clientconfig.Config, clusterName stri
}

// Check RBAC permission via the check-access endpoint
hasAccess, reason, err := checkClusterAccessRBAC(ctx, cfg, identity, clusterXID)
hasAccess, reason, err := checkClusterAccessRBAC(ctx, cfg, cluster.Identity, identity, clusterXID)
if err != nil {
// If we can't reach the cloud API, we'll allow the switch
ctx.Warn("Could not verify cluster access: %v", err)
Expand All @@ -175,9 +174,9 @@ func checkClusterAccess(ctx *Context, cfg *clientconfig.Config, clusterName stri
}

// checkClusterAccessRBAC calls the cloud API to check if the user has RBAC permission to access a cluster
func checkClusterAccessRBAC(ctx *Context, config *clientconfig.Config, identity *clientconfig.IdentityConfig, clusterXID string) (bool, string, error) {
if identity.Type != "keypair" {
return false, "", fmt.Errorf("RBAC check is only supported for keypair identities")
func checkClusterAccessRBAC(ctx *Context, config *clientconfig.Config, identityName string, identity *clientconfig.IdentityConfig, clusterXID string) (bool, string, error) {
if identity.Type != clientconfig.IdentityKeypair && identity.Type != clientconfig.IdentityToken {
return false, "", fmt.Errorf("RBAC check is only supported for keypair and token identities")
}

// Get the issuer URL
Expand All @@ -186,20 +185,8 @@ func checkClusterAccessRBAC(ctx *Context, config *clientconfig.Config, identity
return false, "", fmt.Errorf("identity has no issuer configured")
}

// Get the private key (handles both direct PrivateKey and KeyRef)
privateKeyPEM, err := config.GetPrivateKeyPEM(identity)
if err != nil {
return false, "", fmt.Errorf("failed to get private key: %w", err)
}

// Load the private key
keyPair, err := cloudauth.LoadKeyPairFromPEM(privateKeyPEM)
if err != nil {
return false, "", fmt.Errorf("failed to load private key: %w", err)
}

// Get JWT token
token, err := clientconfig.AuthenticateWithKey(ctx, issuerURL, keyPair)
token, err := config.TokenForIdentity(ctx, identityName, identity, issuerURL)
if err != nil {
return false, "", fmt.Errorf("failed to authenticate: %w", err)
}
Expand Down
25 changes: 7 additions & 18 deletions cli/commands/config_bind_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import (
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"miren.dev/runtime/clientconfig"
"miren.dev/runtime/pkg/cloudauth"
"miren.dev/runtime/pkg/theme"
"miren.dev/runtime/pkg/ui"
)
Expand Down Expand Up @@ -195,10 +194,12 @@ func isPrivateAddress(host string) bool {
return ip.IsPrivate()
}

// fetchAvailableClusters queries the identity server for available clusters
func fetchAvailableClusters(ctx *Context, config *clientconfig.Config, identity *clientconfig.IdentityConfig) ([]ClusterResponse, error) {
if identity.Type != "keypair" {
return nil, fmt.Errorf("cluster listing is only supported for keypair identities")
// fetchAvailableClusters queries the identity server for available clusters.
// identityName may be "" for an anonymous in-memory identity (e.g. during login
// before it has been named), in which case token refreshes are not persisted.
func fetchAvailableClusters(ctx *Context, config *clientconfig.Config, identityName string, identity *clientconfig.IdentityConfig) ([]ClusterResponse, error) {
if identity.Type != clientconfig.IdentityKeypair && identity.Type != clientconfig.IdentityToken {
return nil, fmt.Errorf("cluster listing is only supported for keypair and token identities")
}

// Get the issuer URL
Expand All @@ -207,20 +208,8 @@ func fetchAvailableClusters(ctx *Context, config *clientconfig.Config, identity
return nil, fmt.Errorf("identity has no issuer configured")
}

// Get the private key (handles both direct PrivateKey and KeyRef)
privateKeyPEM, err := config.GetPrivateKeyPEM(identity)
if err != nil {
return nil, fmt.Errorf("failed to get private key: %w", err)
}

// Load the private key
keyPair, err := cloudauth.LoadKeyPairFromPEM(privateKeyPEM)
if err != nil {
return nil, fmt.Errorf("failed to load private key: %w", err)
}

// Get JWT token
token, err := clientconfig.AuthenticateWithKey(ctx, issuerURL, keyPair)
token, err := config.TokenForIdentity(ctx, identityName, identity, issuerURL)
if err != nil {
return nil, fmt.Errorf("failed to authenticate: %w", err)
}
Expand Down
52 changes: 19 additions & 33 deletions cli/commands/debug_connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
"github.com/golang-jwt/jwt/v5"
"github.com/quic-go/quic-go/http3"
"miren.dev/runtime/clientconfig"
"miren.dev/runtime/pkg/cloudauth"
)

// DebugAuthResponse represents the response from the debug-auth endpoint
Expand Down Expand Up @@ -78,6 +77,7 @@ func DebugConnection(ctx *Context, opts struct {
ctx.Info("Step 2: Preparing authentication...")

var identity *clientconfig.IdentityConfig
var identityName string

// Determine which identity to use
if opts.Identity != "" {
Expand All @@ -88,6 +88,7 @@ func DebugConnection(ctx *Context, opts struct {
ctx.Warn("Failed to get identity '%s': %v", opts.Identity, err)
return err
}
identityName = opts.Identity
ctx.Info("Using identity: %s", opts.Identity)
} else {
ctx.Warn("No configuration found. Please run 'miren login' first.")
Expand All @@ -108,18 +109,19 @@ func DebugConnection(ctx *Context, opts struct {
ctx.Warn("Failed to get identity '%s' for cluster '%s': %v", cluster.Identity, opts.Cluster, err)
return err
}
identityName = cluster.Identity
ctx.Info("Using identity '%s' from cluster '%s'", cluster.Identity, opts.Cluster)
} else if cluster.CloudAuth && cluster.ClientKey != "" {
// Backward compatibility: create identity from cluster
identity = &clientconfig.IdentityConfig{
Type: "keypair",
Type: clientconfig.IdentityKeypair,
PrivateKey: cluster.ClientKey,
}
ctx.Info("Using legacy CloudAuth from cluster '%s'", opts.Cluster)
} else if cluster.ClientCert != "" && cluster.ClientKey != "" {
// Backward compatibility: certificate auth
identity = &clientconfig.IdentityConfig{
Type: "certificate",
Type: clientconfig.IdentityCertificate,
ClientCert: cluster.ClientCert,
ClientKey: cluster.ClientKey,
}
Expand All @@ -142,30 +144,14 @@ func DebugConnection(ctx *Context, opts struct {

if identity != nil {
switch identity.Type {
case "keypair":
// For keypair auth, we need to get a JWT token
// The server we're testing should be the cluster, not the auth server

// Get the private key (handles both direct PrivateKey and KeyRef)
var privateKeyPEM string
if config != nil {
privateKeyPEM, err = config.GetPrivateKeyPEM(identity)
if err != nil {
ctx.Warn("Failed to get private key: %v", err)
return err
}
} else if identity.PrivateKey != "" {
// Fallback for backward compatibility (when identity is created on-the-fly)
privateKeyPEM = identity.PrivateKey
} else {
ctx.Warn("Identity has no private key or key reference")
return fmt.Errorf("no private key available")
}

keyPair, err := cloudauth.LoadKeyPairFromPEM(privateKeyPEM)
if err != nil {
ctx.Warn("Failed to load private key: %v", err)
return err
case clientconfig.IdentityKeypair, clientconfig.IdentityToken:
// Both keypair and ephemeral token identities resolve to a JWT bearer
// token. The server we're testing is the cluster, not the auth server.
if config == nil {
// On-the-fly identities (legacy CloudAuth) require a config to
// resolve keys; without one there is nothing to authenticate with.
ctx.Warn("No configuration found. Please run 'miren login' first.")
return fmt.Errorf("no configuration found")
}

// Use the issuer from the identity
Expand All @@ -176,17 +162,17 @@ func DebugConnection(ctx *Context, opts struct {
ctx.Warn("Identity has no issuer configured, using test server as auth server")
}

ctx.Info("Requesting JWT token using keypair from %s...", authServerURL)
ctx.Info("Requesting JWT token for %s identity from %s...", identity.Type, authServerURL)
// Note: This will use the cached token if available
token, err := clientconfig.AuthenticateWithKey(ctx, authServerURL, keyPair)
token, err := config.TokenForIdentity(ctx, identityName, identity, authServerURL)
if err != nil {
ctx.Warn("Failed to authenticate with keypair: %v", err)
ctx.Warn("Failed to authenticate: %v", err)
ctx.Info("Note: Ensure the auth server is reachable at %s", authServerURL)
return err
}

authHeader = "Bearer " + token
authMethod = "keypair/jwt"
authMethod = string(identity.Type) + "/jwt"
ctx.Completed("JWT token obtained")

// Decode and print JWT claims for debugging
Expand All @@ -200,7 +186,7 @@ func DebugConnection(ctx *Context, opts struct {
}
}

case "certificate":
case clientconfig.IdentityCertificate:
// Certificate auth is handled at TLS level
authMethod = "certificate"
ctx.Info("Using client certificate authentication")
Expand Down Expand Up @@ -230,7 +216,7 @@ func DebugConnection(ctx *Context, opts struct {
InsecureSkipVerify: opts.Insecure,
}

if identity != nil && identity.Type == "certificate" {
if identity != nil && identity.Type == clientconfig.IdentityCertificate {
// Add client certificate
cert, err := tls.X509KeyPair([]byte(identity.ClientCert), []byte(identity.ClientKey))
if err != nil {
Expand Down
19 changes: 4 additions & 15 deletions cli/commands/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (
"github.com/charmbracelet/lipgloss"
"miren.dev/runtime/clientconfig"
"miren.dev/runtime/pkg/auth"
"miren.dev/runtime/pkg/cloudauth"
"miren.dev/runtime/pkg/theme"
)

Expand Down Expand Up @@ -98,35 +97,25 @@ func tryAuthenticate(ctx *Context, cfg *clientconfig.Config, cluster *clientconf
result.IdentityName = cluster.Identity

switch identity.Type {
case "keypair":
privateKeyPEM, err := cfg.GetPrivateKeyPEM(identity)
if err != nil {
return result
}

keyPair, err := cloudauth.LoadKeyPairFromPEM(privateKeyPEM)
if err != nil {
return result
}

case clientconfig.IdentityKeypair, clientconfig.IdentityToken:
authServer := identity.Issuer
if authServer == "" {
authServer = cluster.Hostname
}
authServer = normalizeAuthServerURL(authServer)

token, err := clientconfig.AuthenticateWithKey(ctx, authServer, keyPair)
token, err := cfg.TokenForIdentity(ctx, cluster.Identity, identity, authServer)
if err != nil {
return result
}

result.Claims, _ = auth.ParseUnverifiedClaims(token)
result.Method = "keypair"
result.Method = string(identity.Type)

// Fetch user info from cloud
result.UserInfo, _ = fetchCloudUserInfo(ctx, authServer, token)

case "certificate":
case clientconfig.IdentityCertificate:
result.Method = "certificate"
}

Expand Down
2 changes: 1 addition & 1 deletion cli/commands/global.go
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ func (c *Context) wrapRPCError(err error) error {
clusterName = "the cluster"
}

c.Warn("access denied: you don't have permission to access %s\nPlease check your credentials or request access from the cluster administrator", clusterName)
c.Warn("access denied: you don't have permission to access %s\nPlease check your credentials or request access from the cluster administrator.\nIf you were logged in previously, your session may have expired — run 'miren login'.", clusterName)
return ErrAccessDenied
}
return err
Expand Down
Loading
Loading