mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-24 12:49:51 +08:00
support keying the cert on disk by scope qualified subdomain (#68721)
Proliferate Scope Qualified Name in tsh app commands Relax naming length constraints for qualified names
This commit is contained in:
+4
-3
@@ -93,6 +93,7 @@ import (
|
||||
"github.com/gravitational/teleport/lib/multiplexer"
|
||||
"github.com/gravitational/teleport/lib/observability/tracing"
|
||||
libplayer "github.com/gravitational/teleport/lib/player"
|
||||
"github.com/gravitational/teleport/lib/scopes"
|
||||
"github.com/gravitational/teleport/lib/scopes/pinning"
|
||||
"github.com/gravitational/teleport/lib/services"
|
||||
"github.com/gravitational/teleport/lib/session"
|
||||
@@ -3511,17 +3512,17 @@ func (tc *TeleportClient) LogoutDatabase(dbName string) error {
|
||||
}
|
||||
|
||||
// LogoutApp removes key and cert for the specified app.
|
||||
func (tc *TeleportClient) LogoutApp(appName string) error {
|
||||
func (tc *TeleportClient) LogoutApp(appSQN scopes.QualifiedName) error {
|
||||
if tc.localAgent == nil {
|
||||
return nil
|
||||
}
|
||||
if tc.SiteName == "" {
|
||||
return trace.BadParameter("cluster name must be set for app logout")
|
||||
}
|
||||
if appName == "" {
|
||||
if appSQN.Name == "" {
|
||||
return trace.BadParameter("please specify app name to log out of")
|
||||
}
|
||||
return tc.localAgent.DeleteUserCerts(tc.SiteName, WithAppCerts{appName})
|
||||
return tc.localAgent.DeleteUserCerts(tc.SiteName, WithAppCerts{ScopedAppName(appSQN)})
|
||||
}
|
||||
|
||||
// LogoutAllApps removes keys and certs for all apps in the cluster.
|
||||
|
||||
@@ -43,6 +43,7 @@ import (
|
||||
"github.com/gravitational/teleport/lib/auth/authclient"
|
||||
"github.com/gravitational/teleport/lib/cryptosuites"
|
||||
"github.com/gravitational/teleport/lib/resumption"
|
||||
"github.com/gravitational/teleport/lib/scopes"
|
||||
"github.com/gravitational/teleport/lib/services"
|
||||
)
|
||||
|
||||
@@ -262,7 +263,7 @@ func (c *ClusterClient) generateUserCerts(ctx context.Context, cachePolicy CertC
|
||||
keyRing.SSHPrivateKey = newUserKeys.ssh
|
||||
keyRing.Cert = certs.SSH
|
||||
case proto.UserCertsRequest_App:
|
||||
keyRing.AppTLSCredentials[params.RouteToApp.Name] = TLSCredential{
|
||||
keyRing.AppTLSCredentials[ScopedAppName(scopes.QualifiedName{Name: params.RouteToApp.Name, Scope: params.RouteToApp.Scope})] = TLSCredential{
|
||||
PrivateKey: newUserKeys.app,
|
||||
Cert: certs.TLS,
|
||||
}
|
||||
@@ -935,7 +936,7 @@ func PerformSessionMFACeremony(ctx context.Context, params PerformSessionMFACere
|
||||
if keyRing.AppTLSCredentials == nil {
|
||||
keyRing.AppTLSCredentials = make(map[string]TLSCredential)
|
||||
}
|
||||
keyRing.AppTLSCredentials[certsReq.RouteToApp.Name] = TLSCredential{
|
||||
keyRing.AppTLSCredentials[ScopedAppName(scopes.QualifiedName{Name: certsReq.RouteToApp.Name, Scope: certsReq.RouteToApp.Scope})] = TLSCredential{
|
||||
Cert: newCerts.TLS,
|
||||
PrivateKey: params.newUserKeys.app,
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ import (
|
||||
"github.com/gravitational/teleport/api/utils/prompt"
|
||||
"github.com/gravitational/teleport/lib/client"
|
||||
"github.com/gravitational/teleport/lib/kube/kubeconfig"
|
||||
"github.com/gravitational/teleport/lib/scopes"
|
||||
"github.com/gravitational/teleport/lib/sshutils"
|
||||
"github.com/gravitational/teleport/lib/tlsca"
|
||||
"github.com/gravitational/teleport/lib/utils"
|
||||
@@ -805,7 +806,7 @@ func KeyRingFromIdentityFile(identityPath, proxyHost, clusterName string, opts .
|
||||
|
||||
// Similarly, if this identity has any app certs, copy them in.
|
||||
if parsedIdent.RouteToApp.Name != "" {
|
||||
keyRing.AppTLSCredentials[parsedIdent.RouteToApp.Name] = client.TLSCredential{
|
||||
keyRing.AppTLSCredentials[client.ScopedAppName(scopes.QualifiedName{Name: parsedIdent.RouteToApp.Name, Scope: parsedIdent.RouteToApp.Scope})] = client.TLSCredential{
|
||||
// Identity files only have room for one private key and TLS
|
||||
// cert, it must match the app cert.
|
||||
PrivateKey: priv,
|
||||
|
||||
@@ -45,6 +45,8 @@ import (
|
||||
"github.com/gravitational/teleport/api/utils/sshutils"
|
||||
"github.com/gravitational/teleport/lib/auth/authclient"
|
||||
"github.com/gravitational/teleport/lib/cryptosuites"
|
||||
"github.com/gravitational/teleport/lib/scopes"
|
||||
scopedapp "github.com/gravitational/teleport/lib/scopes/app"
|
||||
"github.com/gravitational/teleport/lib/sshca"
|
||||
"github.com/gravitational/teleport/lib/tlsca"
|
||||
"github.com/gravitational/teleport/lib/utils"
|
||||
@@ -580,11 +582,23 @@ func (k *KeyRing) DBTLSCertificates() (certs []x509.Certificate, err error) {
|
||||
return certs, nil
|
||||
}
|
||||
|
||||
// ScopedAppName returns the name that keys an app's TLS credential.
|
||||
// Unscoped apps are keyed by their plain name. Scoped apps are keyed by their
|
||||
// scope-qualified subdomain with a leading "@" prefix: app names must begin
|
||||
// with an alphanumeric character, so the prefix keeps scoped keys separate
|
||||
// from unscoped app names.
|
||||
func ScopedAppName(appSQN scopes.QualifiedName) string {
|
||||
if appSQN.Scope == "" {
|
||||
return appSQN.Name
|
||||
}
|
||||
return "@" + scopedapp.ScopedSubdomain(appSQN.Name, appSQN.Scope)
|
||||
}
|
||||
|
||||
// AppTLSCert returns the tls.Certificate for authentication against a named app.
|
||||
func (k *KeyRing) AppTLSCert(appName string) (tls.Certificate, error) {
|
||||
cred, ok := k.AppTLSCredentials[appName]
|
||||
func (k *KeyRing) AppTLSCert(appSQN scopes.QualifiedName) (tls.Certificate, error) {
|
||||
cred, ok := k.AppTLSCredentials[ScopedAppName(appSQN)]
|
||||
if !ok {
|
||||
return tls.Certificate{}, trace.NotFound("TLS certificate for application %q not found", appName)
|
||||
return tls.Certificate{}, trace.NotFound("TLS certificate for application %q not found", appSQN.Name)
|
||||
}
|
||||
return cred.TLSCertificate()
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import (
|
||||
"github.com/gravitational/teleport/api/utils/keys"
|
||||
"github.com/gravitational/teleport/lib/cryptosuites"
|
||||
"github.com/gravitational/teleport/lib/defaults"
|
||||
"github.com/gravitational/teleport/lib/scopes"
|
||||
"github.com/gravitational/teleport/lib/srv/alpnproxy"
|
||||
"github.com/gravitational/teleport/lib/tlsca"
|
||||
"github.com/gravitational/teleport/lib/utils"
|
||||
@@ -318,7 +319,7 @@ func (c *AppCertIssuer) IssueCert(ctx context.Context) (tls.Certificate, error)
|
||||
return tls.Certificate{}, trace.Wrap(err)
|
||||
}
|
||||
|
||||
appCert, err := keyRing.AppTLSCert(c.RouteToApp.Name)
|
||||
appCert, err := keyRing.AppTLSCert(scopes.QualifiedName{Name: c.RouteToApp.Name, Scope: c.RouteToApp.Scope})
|
||||
if err != nil {
|
||||
return tls.Certificate{}, trace.Wrap(err)
|
||||
}
|
||||
|
||||
+2
-1
@@ -34,6 +34,7 @@ import (
|
||||
"github.com/gravitational/teleport/api/client/proto"
|
||||
apidefaults "github.com/gravitational/teleport/api/defaults"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/lib/scopes"
|
||||
alpncommon "github.com/gravitational/teleport/lib/srv/alpnproxy/common"
|
||||
"github.com/gravitational/teleport/lib/utils"
|
||||
)
|
||||
@@ -167,7 +168,7 @@ func (d *MCPServerDialer) getCertLocked(ctx context.Context, mcpServer types.App
|
||||
return tls.Certificate{}, trace.Wrap(err)
|
||||
}
|
||||
|
||||
cert, err := keyRing.AppTLSCert(mcpServer.GetName())
|
||||
cert, err := keyRing.AppTLSCert(scopes.QualifiedName{Name: appCertParams.RouteToApp.Name, Scope: appCertParams.RouteToApp.Scope})
|
||||
if err != nil {
|
||||
return tls.Certificate{}, trace.Wrap(err)
|
||||
}
|
||||
|
||||
+18
-6
@@ -37,6 +37,7 @@ import (
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/api/types/wrappers"
|
||||
"github.com/gravitational/teleport/api/utils/keypaths"
|
||||
"github.com/gravitational/teleport/lib/scopes"
|
||||
"github.com/gravitational/teleport/lib/services"
|
||||
"github.com/gravitational/teleport/lib/sshca"
|
||||
"github.com/gravitational/teleport/lib/tlsca"
|
||||
@@ -609,11 +610,15 @@ func (p *ProfileStatus) DatabaseLocalCAPath() string {
|
||||
// AppCertPath returns path to the specified app access certificate
|
||||
// for this profile.
|
||||
//
|
||||
// It's kept in <profile-dir>/keys/<proxy>/<user>-app/<cluster>/<name>.crt
|
||||
func (p *ProfileStatus) AppCertPath(cluster, name string) string {
|
||||
// It's kept in <profile-dir>/keys/<proxy>/<user>-app/<cluster>/<name>.crt for
|
||||
// unscoped apps.
|
||||
// Scoped apps are kept in
|
||||
// <profile-dir>/keys/<proxy>/<user>-app/<cluster>/@<scope-qualified-subdomain>.crt.
|
||||
func (p *ProfileStatus) AppCertPath(cluster string, appSQN scopes.QualifiedName) string {
|
||||
if cluster == "" {
|
||||
cluster = p.Cluster
|
||||
}
|
||||
name := ScopedAppName(appSQN)
|
||||
if path, ok := p.virtualPathFromEnv(VirtualPathAppCert, VirtualPathAppCertParams(name)); ok {
|
||||
return path
|
||||
}
|
||||
@@ -624,11 +629,15 @@ func (p *ProfileStatus) AppCertPath(cluster, name string) string {
|
||||
// AppKeyPath returns path to the specified app access private key for this
|
||||
// profile.
|
||||
//
|
||||
// It's kept in <profile-dir>/keys/<proxy>/<user>-app/<cluster>/<name>.key
|
||||
func (p *ProfileStatus) AppKeyPath(cluster, name string) string {
|
||||
// It's kept in <profile-dir>/keys/<proxy>/<user>-app/<cluster>/<name>.key for
|
||||
// unscoped apps.
|
||||
// Scoped apps are kept in
|
||||
// <profile-dir>/keys/<proxy>/<user>-app/<cluster>/@<scope-qualified-subdomain>.key
|
||||
func (p *ProfileStatus) AppKeyPath(cluster string, appSQN scopes.QualifiedName) string {
|
||||
if cluster == "" {
|
||||
cluster = p.Cluster
|
||||
}
|
||||
name := ScopedAppName(appSQN)
|
||||
if path, ok := p.virtualPathFromEnv(VirtualPathKey, VirtualPathAppKeyParams(name)); ok {
|
||||
return path
|
||||
}
|
||||
@@ -640,11 +649,14 @@ func (p *ProfileStatus) AppKeyPath(cluster, name string) string {
|
||||
// this profile.
|
||||
//
|
||||
// It's kept in <profile-dir>/keys/<proxy>/<user>-app/<cluster>/<name>-localca.pem
|
||||
func (p *ProfileStatus) AppLocalCAPath(cluster, name string) string {
|
||||
// for unscoped apps.
|
||||
// Scoped apps are kept in
|
||||
// <profile-dir>/keys/<proxy>/<user>-app/<cluster>/@<scope-qualified-subdomain>-localca.pem.
|
||||
func (p *ProfileStatus) AppLocalCAPath(cluster string, appSQN scopes.QualifiedName) string {
|
||||
if cluster == "" {
|
||||
cluster = p.Cluster
|
||||
}
|
||||
return keypaths.AppLocalCAPath(p.Dir, p.Name, p.Username, cluster, name)
|
||||
return keypaths.AppLocalCAPath(p.Dir, p.Name, p.Username, cluster, ScopedAppName(appSQN))
|
||||
}
|
||||
|
||||
// KubeConfigPath returns path to the specified kubeconfig for this profile.
|
||||
|
||||
@@ -57,6 +57,11 @@ func scopeSubdomainOk(subdomain string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// ScopedSubdomain returns the scope-qualified subdomain for a scoped app.
|
||||
func ScopedSubdomain(appName, scope string) string {
|
||||
return generateScopedSubDomain(appName, scope)
|
||||
}
|
||||
|
||||
// ScopedAppPublicAddr returns the derived public address for a scoped app (Scope Qualified SubDomain):
|
||||
// "<ScopedSubdomain(name, scope)>.<proxy>".
|
||||
// The trailing port on the proxy is stripped and the host is lowercased so the result
|
||||
|
||||
+35
-1
@@ -19,6 +19,7 @@
|
||||
package scopes
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
@@ -55,6 +56,25 @@ func (q QualifiedName) String() string {
|
||||
return q.Scope + QualifiedNameSeparator + q.Name
|
||||
}
|
||||
|
||||
// Set sets a possible scope qualified name. If the "::" separator is not present,
|
||||
// then the scope qualified name will have an empty scope. This implements
|
||||
// the flag/kingping Value interface.
|
||||
func (q *QualifiedName) Set(val string) error {
|
||||
if !strings.Contains(val, QualifiedNameSeparator) {
|
||||
*q = QualifiedName{Name: val}
|
||||
return nil
|
||||
}
|
||||
sqn, err := ParseQualifiedName(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := sqn.StrongValidate(); err != nil {
|
||||
return err
|
||||
}
|
||||
*q = sqn
|
||||
return nil
|
||||
}
|
||||
|
||||
// StrongValidate validates this QualifiedName using strong validation rules. This method
|
||||
// *must* be called on all QualifiedName values derived from user input and/or cluster-external
|
||||
// sources. Use [QualifiedName.WeakValidate] when checking values from the control plane in
|
||||
@@ -64,7 +84,7 @@ func (q QualifiedName) StrongValidate() error {
|
||||
return trace.BadParameter("scope-qualified name %q has invalid scope: %v", q, err)
|
||||
}
|
||||
|
||||
if err := StrongValidateSegment(q.Name); err != nil {
|
||||
if err := strongValidateName(q.Name); err != nil {
|
||||
return trace.BadParameter("scope-qualified name %q has invalid name: %v", q, err)
|
||||
}
|
||||
|
||||
@@ -93,6 +113,20 @@ func (q QualifiedName) WeakValidate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// nameRegexp is the regular expression used to validate scoped resource names. It enforces
|
||||
// the same character rules as segmentRegexp, but allows for single character names.
|
||||
var nameRegexp = regexp.MustCompile(`^[a-z0-9]([a-z0-9\-\_\.]*[a-z0-9])?$`)
|
||||
|
||||
// strongValidateName checks if a scoped resource name is valid according to scope character
|
||||
// formatting rules. Unlike scope segments, names carry no length constraints.
|
||||
func strongValidateName(name string) error {
|
||||
if name == "" {
|
||||
return trace.BadParameter("name is empty")
|
||||
}
|
||||
|
||||
return trace.Wrap(strongValidateFormat(name, nameRegexp))
|
||||
}
|
||||
|
||||
// ParseQualifiedName parses a scope-qualified name string into its scope and name
|
||||
// components by splitting on the first occurrence of "::". Returns an error if the
|
||||
// separator is absent or either component is empty. This function does not validate
|
||||
|
||||
@@ -252,9 +252,9 @@ func TestValidateQualifiedName(t *testing.T) {
|
||||
weakOk: true,
|
||||
},
|
||||
{
|
||||
name: "name too short",
|
||||
name: "single-character name",
|
||||
sqn: "/staging::x",
|
||||
strongOk: false,
|
||||
strongOk: true,
|
||||
weakOk: true,
|
||||
},
|
||||
{
|
||||
@@ -297,3 +297,53 @@ func TestValidateQualifiedName(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSet(t *testing.T) {
|
||||
t.Parallel()
|
||||
tts := []struct {
|
||||
name string
|
||||
val string
|
||||
ok bool
|
||||
expected QualifiedName
|
||||
}{
|
||||
{
|
||||
name: "only name provided",
|
||||
val: "bare-name",
|
||||
ok: true,
|
||||
expected: QualifiedName{Name: "bare-name"},
|
||||
},
|
||||
{
|
||||
name: "valid scope and name",
|
||||
val: "/scope::name",
|
||||
|
||||
ok: true,
|
||||
expected: QualifiedName{Name: "name", Scope: "/scope"},
|
||||
},
|
||||
{
|
||||
name: "invalid name",
|
||||
val: "/scope::!!name",
|
||||
ok: false,
|
||||
},
|
||||
{
|
||||
name: "invalid scope",
|
||||
val: "!bad::test",
|
||||
ok: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tts {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var sqn QualifiedName
|
||||
err := sqn.Set(tt.val)
|
||||
if tt.ok {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.Equal(t, tt.expected, sqn)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+17
-5
@@ -144,6 +144,22 @@ func StrongValidateSegment(segment string) error {
|
||||
return trace.BadParameter("segment %q is too short (min characters %d)", segment, minSegmentSize)
|
||||
}
|
||||
|
||||
if err := strongValidateFormat(segment, segmentRegexp); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
if len(segment) > maxSegmentSize {
|
||||
return trace.BadParameter("segment %q is too long (max characters %d)", segment, maxSegmentSize)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// strongValidateFormat applies the strong formatting checks:
|
||||
// - no uppercase characters
|
||||
// - the provided shape regexp
|
||||
// - weak checks as a defensive backstop
|
||||
func strongValidateFormat(segment string, shape *regexp.Regexp) error {
|
||||
// check for uppercase characters separately. this would be caught by the regex, but its better
|
||||
// UX to call out uppercase characters specifically since its a common mistake.
|
||||
for _, r := range segment {
|
||||
@@ -152,14 +168,10 @@ func StrongValidateSegment(segment string) error {
|
||||
}
|
||||
}
|
||||
|
||||
if !segmentRegexp.MatchString(segment) {
|
||||
if !shape.MatchString(segment) {
|
||||
return trace.BadParameter("segment %q is malformed", segment)
|
||||
}
|
||||
|
||||
if len(segment) > maxSegmentSize {
|
||||
return trace.BadParameter("segment %q is too long (max characters %d)", segment, maxSegmentSize)
|
||||
}
|
||||
|
||||
// as an extra precaution, also run all weak checks just to be certain we didn't accidentally
|
||||
// construct a weak check that rejects something that would otherwise pass a strong check. strong
|
||||
// validation is not used in perf-critical paths, so there isn't any real downside to a little
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/lib/auth/authclient"
|
||||
"github.com/gravitational/teleport/lib/client"
|
||||
"github.com/gravitational/teleport/lib/scopes"
|
||||
"github.com/gravitational/teleport/lib/teleterm/api/uri"
|
||||
"github.com/gravitational/teleport/lib/utils"
|
||||
"github.com/gravitational/teleport/lib/utils/aws"
|
||||
@@ -92,7 +93,7 @@ func (c *Cluster) ReissueAppCert(ctx context.Context, clusterClient *client.Clus
|
||||
return tls.Certificate{}, trace.Wrap(err)
|
||||
}
|
||||
|
||||
appCert, err := result.KeyRing.AppTLSCert(routeToApp.Name)
|
||||
appCert, err := result.KeyRing.AppTLSCert(scopes.QualifiedName{Name: routeToApp.Name, Scope: routeToApp.Scope})
|
||||
return appCert, trace.Wrap(err)
|
||||
}
|
||||
|
||||
|
||||
+28
-57
@@ -48,31 +48,8 @@ import (
|
||||
logutils "github.com/gravitational/teleport/lib/utils/log"
|
||||
)
|
||||
|
||||
// parseScopeQualifiedAppName splits a scope-qualified app name argument
|
||||
// ("/scope::name") into cf.AppScope and cf.AppName. Bare names are left
|
||||
// untouched and refer to unscoped apps.
|
||||
func parseScopeQualifiedAppName(cf *CLIConf) error {
|
||||
if !strings.Contains(cf.AppName, scopes.QualifiedNameSeparator) {
|
||||
return nil
|
||||
}
|
||||
qn, err := scopes.ParseQualifiedName(cf.AppName)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
if err := scopes.StrongValidate(qn.Scope); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
cf.AppScope = qn.Scope
|
||||
cf.AppName = qn.Name
|
||||
return nil
|
||||
}
|
||||
|
||||
// onAppLogin implements "tsh apps login" command.
|
||||
func onAppLogin(cf *CLIConf) error {
|
||||
if err := parseScopeQualifiedAppName(cf); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
tc, err := makeClient(cf)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
@@ -457,9 +434,6 @@ Example command: tsh gcloud compute instances list
|
||||
|
||||
// onAppLogout implements "tsh apps logout" command.
|
||||
func onAppLogout(cf *CLIConf) error {
|
||||
if err := parseScopeQualifiedAppName(cf); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
tc, err := makeClient(cf)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
@@ -477,19 +451,19 @@ func onAppLogout(cf *CLIConf) error {
|
||||
// If a specific app name was specified, just log out of that app.
|
||||
// Otherwise, log out of all apps.
|
||||
var logout []tlsca.RouteToApp
|
||||
if cf.AppName != "" {
|
||||
if cf.AppSQN.Name != "" {
|
||||
for _, app := range activeRoutes {
|
||||
if app.Name == cf.AppName && app.Scope == cf.AppScope {
|
||||
if app.Name == cf.AppSQN.Name && app.Scope == cf.AppSQN.Scope {
|
||||
logout = append(logout, app)
|
||||
}
|
||||
}
|
||||
|
||||
if len(logout) == 0 {
|
||||
// Not logged in but still try to delete any dangling files.
|
||||
if err := tc.LogoutApp(cf.AppName); err != nil {
|
||||
if err := tc.LogoutApp(cf.AppSQN); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
return trace.BadParameter("not logged into app %q", cf.AppName)
|
||||
return trace.BadParameter("not logged into app %q", cf.AppSQN)
|
||||
}
|
||||
} else {
|
||||
logout = activeRoutes
|
||||
@@ -501,27 +475,27 @@ func onAppLogout(cf *CLIConf) error {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
err = tc.LogoutApp(app.Name)
|
||||
err = tc.LogoutApp(scopes.QualifiedName{Name: app.Name, Scope: app.Scope})
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
// remove generated local files for the provided app.
|
||||
err := utils.RemoveFileIfExist(profile.AppLocalCAPath(tc.SiteName, app.Name))
|
||||
err := utils.RemoveFileIfExist(profile.AppLocalCAPath(tc.SiteName, scopes.QualifiedName{Name: app.Name, Scope: app.Scope}))
|
||||
if err != nil {
|
||||
logger.WarnContext(cf.Context, "Failed to clean up app session",
|
||||
"error", err,
|
||||
"profile", profile.AppLocalCAPath(tc.SiteName, app.Name))
|
||||
"profile", profile.AppLocalCAPath(tc.SiteName, scopes.QualifiedName{Name: app.Name, Scope: app.Scope}))
|
||||
}
|
||||
|
||||
if err := removeExternalFilesForApp(app); err != nil {
|
||||
logger.WarnContext(cf.Context, "Failed to clean up app external files",
|
||||
"error", err,
|
||||
"app", cf.AppName)
|
||||
"app", cf.AppSQN)
|
||||
}
|
||||
}
|
||||
|
||||
if cf.AppName == "" {
|
||||
if cf.AppSQN.Name == "" {
|
||||
// Try to delete any dangling files even if the app sessions are expired.
|
||||
if err := tc.LogoutAllApps(); err != nil {
|
||||
return trace.Wrap(err)
|
||||
@@ -534,7 +508,7 @@ func onAppLogout(cf *CLIConf) error {
|
||||
}
|
||||
|
||||
if len(logout) == 1 {
|
||||
fmt.Printf("Logged out of app %q\n", logout[0].Name)
|
||||
fmt.Printf("Logged out of app %q\n", scopes.QualifiedName{Name: logout[0].Name, Scope: logout[0].Scope}.String())
|
||||
} else {
|
||||
fmt.Println("Logged out of all apps")
|
||||
}
|
||||
@@ -543,9 +517,6 @@ func onAppLogout(cf *CLIConf) error {
|
||||
|
||||
// onAppConfig implements "tsh apps config" command.
|
||||
func onAppConfig(cf *CLIConf) error {
|
||||
if err := parseScopeQualifiedAppName(cf); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
tc, err := makeClient(cf)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
@@ -602,10 +573,10 @@ func formatAppConfig(tc *client.TeleportClient, profile *client.ProfileStatus, r
|
||||
if tc.InsecureSkipVerify {
|
||||
curlInsecureFlag = "--insecure "
|
||||
}
|
||||
|
||||
certPath := profile.AppCertPath(tc.SiteName, routeToApp.Name)
|
||||
keyPath := profile.AppKeyPath(tc.SiteName, routeToApp.Name)
|
||||
appName := scopes.QualifiedName{Name: routeToApp.Name, Scope: routeToApp.Scope}.String()
|
||||
appSQN := scopes.QualifiedName{Name: routeToApp.Name, Scope: routeToApp.Scope}
|
||||
certPath := profile.AppCertPath(tc.SiteName, appSQN)
|
||||
keyPath := profile.AppKeyPath(tc.SiteName, appSQN)
|
||||
appName := appSQN.String()
|
||||
|
||||
curlCmd := fmt.Sprintf(`curl %s\
|
||||
--cert %q \
|
||||
@@ -734,7 +705,7 @@ func getAppInfo(cf *CLIConf, clt authclient.ClientI, profile *client.ProfileStat
|
||||
}
|
||||
|
||||
// If we didn't find an active profile for the app, get info from server.
|
||||
app, logins, err := getApp(cf.Context, clt, cf.AppName, cf.AppScope)
|
||||
app, logins, err := getApp(cf.Context, clt, cf.AppSQN)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
@@ -822,7 +793,7 @@ func (a *appInfo) GetApp(ctx context.Context, clt apiclient.GetResourcesClient)
|
||||
return a.app.Copy(), nil
|
||||
}
|
||||
// holding mutex across the api call to avoid multiple redundant api calls.
|
||||
app, _, err := getApp(ctx, clt, a.Name, a.RouteToApp.Scope)
|
||||
app, _, err := getApp(ctx, clt, scopes.QualifiedName{Name: a.GetName(), Scope: a.GetScope()})
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
@@ -833,7 +804,7 @@ func (a *appInfo) GetApp(ctx context.Context, clt apiclient.GetResourcesClient)
|
||||
// getApp returns the registered application with the specified name and scope.
|
||||
// A bare name (empty scope) only ever resolves to an unscoped app; scoped apps
|
||||
// must be referenced by their scope-qualified name.
|
||||
func getApp(ctx context.Context, clt apiclient.GetResourcesClient, name, scope string) (app types.Application, logins []string, err error) {
|
||||
func getApp(ctx context.Context, clt apiclient.GetResourcesClient, appSQN scopes.QualifiedName) (app types.Application, logins []string, err error) {
|
||||
// A (name, scope) pair identifies a single logical app.
|
||||
// if a user does not supply a scope, we need to further parse the results to make sure
|
||||
// that the results do not contain scopes - disallowing cross scope app access.
|
||||
@@ -842,10 +813,10 @@ func getApp(ctx context.Context, clt apiclient.GetResourcesClient, name, scope s
|
||||
//
|
||||
// TODO (williamo/scopes): eventually revert back to retrieving just 1 result and pass the scope expression by
|
||||
// default, and delete the client side filtering.
|
||||
predicate := fmt.Sprintf("name == %q", name)
|
||||
predicate := fmt.Sprintf("name == %q", appSQN.Name)
|
||||
limit := apidefaults.DefaultChunkSize
|
||||
if scope != "" {
|
||||
predicate = fmt.Sprintf("name == %q && resource.scope == %q", name, scope)
|
||||
if appSQN.Scope != "" {
|
||||
predicate = fmt.Sprintf("name == %q && resource.scope == %q", appSQN.Name, appSQN.Scope)
|
||||
limit = 1
|
||||
}
|
||||
res, err := apiclient.GetEnrichedResourcePage(ctx, clt, &proto.ListResourcesRequest{
|
||||
@@ -868,12 +839,12 @@ func getApp(ctx context.Context, clt apiclient.GetResourcesClient, name, scope s
|
||||
}
|
||||
|
||||
a := server.GetApp()
|
||||
if a.GetScope() == scope {
|
||||
if a.GetScope() == appSQN.Scope {
|
||||
return a, r.Logins, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil, trace.NotFound("app %q not found, use `tsh apps ls` to see registered apps", name)
|
||||
return nil, nil, trace.NotFound("app %q not found, use `tsh apps ls` to see registered apps", appSQN.String())
|
||||
}
|
||||
|
||||
// pickActiveApp returns the app the current profile is logged into.
|
||||
@@ -881,7 +852,7 @@ func getApp(ctx context.Context, clt apiclient.GetResourcesClient, name, scope s
|
||||
// If logged into multiple apps, returns an error unless one was specified
|
||||
// explicitly on CLI.
|
||||
func pickActiveApp(cf *CLIConf, activeRoutes []tlsca.RouteToApp) (proto.RouteToApp, error) {
|
||||
if cf.AppName == "" {
|
||||
if cf.AppSQN.Name == "" {
|
||||
switch len(activeRoutes) {
|
||||
case 0:
|
||||
return proto.RouteToApp{}, trace.NotFound("please login using 'tsh apps login' first")
|
||||
@@ -898,14 +869,14 @@ func pickActiveApp(cf *CLIConf, activeRoutes []tlsca.RouteToApp) (proto.RouteToA
|
||||
}
|
||||
|
||||
for _, r := range activeRoutes {
|
||||
if r.Name == cf.AppName && r.Scope == cf.AppScope {
|
||||
if r.Name == cf.AppSQN.Name && r.Scope == cf.AppSQN.Scope {
|
||||
return tlscaRouteToAppToProto(r), nil
|
||||
}
|
||||
}
|
||||
if cf.AppScope != "" {
|
||||
return proto.RouteToApp{}, trace.NotFound("not logged into app %q in scope %q", cf.AppName, cf.AppScope)
|
||||
if cf.AppSQN.Scope != "" {
|
||||
return proto.RouteToApp{}, trace.NotFound("not logged into app %q in scope %q", cf.AppSQN.Name, cf.AppSQN.Scope)
|
||||
}
|
||||
return proto.RouteToApp{}, trace.NotFound("not logged into app %q", cf.AppName)
|
||||
return proto.RouteToApp{}, trace.NotFound("not logged into app %q", cf.AppSQN.Name)
|
||||
}
|
||||
|
||||
func tlscaRouteToAppToProto(route tlsca.RouteToApp) proto.RouteToApp {
|
||||
@@ -940,7 +911,7 @@ func onAppLogins(cf *CLIConf) error {
|
||||
}
|
||||
defer clusterClient.Close()
|
||||
|
||||
app, logins, err = getApp(cf.Context, clusterClient.AuthClient, cf.AppName, cf.AppScope)
|
||||
app, logins, err = getApp(cf.Context, clusterClient.AuthClient, cf.AppSQN)
|
||||
return trace.Wrap(err)
|
||||
}); err != nil {
|
||||
return trace.Wrap(err)
|
||||
|
||||
@@ -37,6 +37,7 @@ import (
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/lib/asciitable"
|
||||
"github.com/gravitational/teleport/lib/client"
|
||||
"github.com/gravitational/teleport/lib/scopes"
|
||||
"github.com/gravitational/teleport/lib/services"
|
||||
"github.com/gravitational/teleport/lib/srv/alpnproxy"
|
||||
"github.com/gravitational/teleport/lib/tlsca"
|
||||
@@ -169,7 +170,7 @@ func (a *awsApp) GetEnvVars() (map[string]string, error) {
|
||||
// https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html
|
||||
"AWS_ACCESS_KEY_ID": cred.AccessKeyID,
|
||||
"AWS_SECRET_ACCESS_KEY": cred.SecretAccessKey,
|
||||
"AWS_CA_BUNDLE": a.profile.AppLocalCAPath(a.cf.SiteName, a.routeToApp.Name),
|
||||
"AWS_CA_BUNDLE": a.profile.AppLocalCAPath(a.cf.SiteName, scopes.QualifiedName{Name: a.routeToApp.Name, Scope: a.routeToApp.Scope}),
|
||||
// Proxy settings.
|
||||
"HTTPS_PROXY": "http://" + a.localForwardProxy.GetAddr(),
|
||||
"https_proxy": "http://" + a.localForwardProxy.GetAddr(),
|
||||
|
||||
@@ -40,6 +40,7 @@ import (
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/lib/asciitable"
|
||||
"github.com/gravitational/teleport/lib/client"
|
||||
"github.com/gravitational/teleport/lib/scopes"
|
||||
"github.com/gravitational/teleport/lib/srv/alpnproxy"
|
||||
"github.com/gravitational/teleport/lib/tlsca"
|
||||
"github.com/gravitational/teleport/lib/utils"
|
||||
@@ -245,7 +246,7 @@ func (a *azureApp) GetEnvVars() (map[string]string, error) {
|
||||
// This isn't portable and applications other than az CLI may have to set different env variables,
|
||||
// add the application cert to system root store (not recommended, ultimate fallback)
|
||||
// or use equivalent of --insecure flag.
|
||||
"REQUESTS_CA_BUNDLE": a.profile.AppLocalCAPath(a.cf.SiteName, a.routeToApp.Name),
|
||||
"REQUESTS_CA_BUNDLE": a.profile.AppLocalCAPath(a.cf.SiteName, scopes.QualifiedName{Name: a.routeToApp.Name, Scope: a.routeToApp.Scope}),
|
||||
}
|
||||
|
||||
if a.usingMSAL() {
|
||||
|
||||
@@ -35,6 +35,7 @@ import (
|
||||
"github.com/gravitational/teleport/lib/asciitable"
|
||||
"github.com/gravitational/teleport/lib/client"
|
||||
"github.com/gravitational/teleport/lib/defaults"
|
||||
"github.com/gravitational/teleport/lib/scopes"
|
||||
"github.com/gravitational/teleport/lib/srv/alpnproxy"
|
||||
"github.com/gravitational/teleport/lib/tlsca"
|
||||
"github.com/gravitational/teleport/lib/utils"
|
||||
@@ -241,7 +242,7 @@ func (a *gcpApp) GetEnvVars() (map[string]string, error) {
|
||||
|
||||
// Set core.custom_ca_certs_file via env variable, customizing the path to CA certs file.
|
||||
// https://cloud.google.com/sdk/gcloud/reference/config/set#:~:text=custom_ca_certs_file
|
||||
"CLOUDSDK_CORE_CUSTOM_CA_CERTS_FILE": a.profile.AppLocalCAPath(a.cf.SiteName, a.routeToApp.Name),
|
||||
"CLOUDSDK_CORE_CUSTOM_CA_CERTS_FILE": a.profile.AppLocalCAPath(a.cf.SiteName, scopes.QualifiedName{Name: a.routeToApp.Name, Scope: a.routeToApp.Scope}),
|
||||
|
||||
// We need to set project ID. This is sourced from the account name.
|
||||
// https://cloud.google.com/sdk/gcloud/reference/config#GROUP:~:text=authentication%20to%20gsutil.-,project,-Project%20ID%20of
|
||||
|
||||
@@ -31,6 +31,7 @@ import (
|
||||
"github.com/gravitational/teleport/api/client/proto"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/lib/client"
|
||||
"github.com/gravitational/teleport/lib/scopes"
|
||||
"github.com/gravitational/teleport/lib/srv/alpnproxy"
|
||||
)
|
||||
|
||||
@@ -159,7 +160,7 @@ func (a *localProxyApp) startLocalALPNProxy(ctx context.Context, portMapping cli
|
||||
|
||||
// If a stored cert is found for the app, try using it.
|
||||
// Otherwise, let the checker reissue one as needed.
|
||||
cert, err := loadAppCertificate(a.tc, routeToAppWithTargetPort.Name)
|
||||
cert, err := loadAppCertificate(a.tc, routeToAppWithTargetPort.Name, routeToAppWithTargetPort.Scope)
|
||||
if err == nil {
|
||||
if a.app != nil && len(a.app.GetTCPPorts()) > 0 {
|
||||
// There are too many cases to cover when dealing with a multi-port app and an existing cert.
|
||||
@@ -176,7 +177,7 @@ func (a *localProxyApp) startLocalALPNProxy(ctx context.Context, portMapping cli
|
||||
|
||||
var listener net.Listener
|
||||
if withTLS {
|
||||
appLocalCAPath := a.profile.AppLocalCAPath(a.tc.SiteName, routeToAppWithTargetPort.Name)
|
||||
appLocalCAPath := a.profile.AppLocalCAPath(a.tc.SiteName, scopes.QualifiedName{Name: routeToAppWithTargetPort.Name, Scope: routeToAppWithTargetPort.Scope})
|
||||
localCertGenerator, err := client.NewLocalCertGenerator(ctx, appCertChecker, appLocalCAPath)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
|
||||
@@ -51,7 +51,7 @@ func newMCPConnectCommand(parent *kingpin.CmdClause, cf *CLIConf) *mcpConnectCom
|
||||
cf: cf,
|
||||
}
|
||||
|
||||
cmd.Arg("name", "Name of the MCP server.").Required().StringVar(&cf.AppName)
|
||||
cmd.Arg("name", "Name of the MCP server.").Required().SetValue(&cf.AppSQN)
|
||||
cmd.Flag("auto-reconnect", mcpAutoReconnectHelp).Default("true").BoolVar(&cmd.autoReconnect)
|
||||
cmd.Flag("header", "Extra custom headers used for streamable HTTP MCP servers.").Short('H').StringsVar(&cmd.httpHeaders)
|
||||
return cmd
|
||||
@@ -81,7 +81,7 @@ func newMCPConfigCommand(parent *kingpin.CmdClause, cf *CLIConf) *mcpConfigComma
|
||||
cmd.Flag("labels", labelHelp).StringVar(&cf.Labels)
|
||||
cmd.Flag("query", queryHelp).StringVar(&cf.PredicateExpression)
|
||||
cmd.Flag("auto-reconnect", mcpAutoReconnectHelp).IsSetByUser(&cmd.autoReconnectSetByUser).BoolVar(&cmd.autoReconnect)
|
||||
cmd.Arg("name", "Name of the MCP server.").StringVar(&cf.AppName)
|
||||
cmd.Arg("name", "Name of the MCP server.").SetValue(&cf.AppSQN)
|
||||
cmd.clientConfig.addToCmd(cmd.CmdClause)
|
||||
cmd.Alias(mcpConfigHelp)
|
||||
cmd.Flag("header", "Extra custom headers used for streamable HTTP MCP servers.").Short('H').StringsVar(&cmd.httpHeaders)
|
||||
@@ -323,7 +323,7 @@ func (c *mcpConfigCommand) checkSelectorFlags() error {
|
||||
var mutuallyExclusiveSelectors int
|
||||
for _, selectorEnabled := range []bool{
|
||||
c.cf.ListAll,
|
||||
c.cf.AppName != "",
|
||||
c.cf.AppSQN.Name != "",
|
||||
c.cf.PredicateExpression != "",
|
||||
c.cf.Labels != "",
|
||||
} {
|
||||
@@ -356,8 +356,8 @@ func (c *mcpConfigCommand) fetchAndPrintResult() error {
|
||||
}
|
||||
|
||||
func (c *mcpConfigCommand) fetch() error {
|
||||
if c.cf.AppName != "" {
|
||||
c.cf.PredicateExpression = makeNamePredicate(c.cf.AppName)
|
||||
if c.cf.AppSQN.Name != "" {
|
||||
c.cf.PredicateExpression = makeNamePredicate(c.cf.AppSQN.Name)
|
||||
}
|
||||
if c.fetchFunc == nil {
|
||||
c.fetchFunc = fetchMCPServers
|
||||
@@ -488,7 +488,7 @@ func (c *mcpConnectCommand) run() error {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
dialer := client.NewMCPServerDialer(tc, c.cf.AppName)
|
||||
dialer := client.NewMCPServerDialer(tc, c.cf.AppSQN.Name)
|
||||
return clientmcp.ProxyStdioConn(
|
||||
c.cf.Context,
|
||||
clientmcp.ProxyStdioConnConfig{
|
||||
|
||||
@@ -36,6 +36,7 @@ import (
|
||||
"github.com/gravitational/teleport/lib/client"
|
||||
mcpconfig "github.com/gravitational/teleport/lib/client/mcp/config"
|
||||
"github.com/gravitational/teleport/lib/observability/tracing"
|
||||
"github.com/gravitational/teleport/lib/scopes"
|
||||
"github.com/gravitational/teleport/lib/services"
|
||||
"github.com/gravitational/teleport/lib/utils/testutils/golden"
|
||||
)
|
||||
@@ -225,7 +226,7 @@ func Test_mcpConfigCommand(t *testing.T) {
|
||||
{
|
||||
name: "not found",
|
||||
cf: &CLIConf{
|
||||
AppName: "not found",
|
||||
AppSQN: scopes.QualifiedName{Name: "not found"},
|
||||
},
|
||||
checkError: require.Error,
|
||||
// "local-everything" was already in the config. Double-check we
|
||||
@@ -235,7 +236,7 @@ func Test_mcpConfigCommand(t *testing.T) {
|
||||
{
|
||||
name: "single",
|
||||
cf: &CLIConf{
|
||||
AppName: "dev2",
|
||||
AppSQN: scopes.QualifiedName{Name: "dev2"},
|
||||
},
|
||||
checkError: require.NoError,
|
||||
wantNamesInConfig: []string{"teleport-mcp-dev2", "local-everything"},
|
||||
@@ -269,7 +270,7 @@ func Test_mcpConfigCommand(t *testing.T) {
|
||||
name: "too many selectors",
|
||||
cf: &CLIConf{
|
||||
ListAll: true,
|
||||
AppName: "dev2",
|
||||
AppSQN: scopes.QualifiedName{Name: "dev2"},
|
||||
},
|
||||
checkError: require.Error,
|
||||
wantNamesInConfig: []string{"local-everything"},
|
||||
@@ -277,7 +278,7 @@ func Test_mcpConfigCommand(t *testing.T) {
|
||||
{
|
||||
name: "print json",
|
||||
cf: &CLIConf{
|
||||
AppName: "dev2",
|
||||
AppSQN: scopes.QualifiedName{Name: "dev2"},
|
||||
},
|
||||
disableConfigFile: true,
|
||||
checkError: require.NoError,
|
||||
@@ -288,7 +289,7 @@ func Test_mcpConfigCommand(t *testing.T) {
|
||||
{
|
||||
name: "custom headers",
|
||||
cf: &CLIConf{
|
||||
AppName: "streamable-http",
|
||||
AppSQN: scopes.QualifiedName{Name: "streamable-http"},
|
||||
},
|
||||
inputHeaderArgs: []string{
|
||||
"H1: v1",
|
||||
@@ -305,7 +306,7 @@ func Test_mcpConfigCommand(t *testing.T) {
|
||||
{
|
||||
name: "invalid header",
|
||||
cf: &CLIConf{
|
||||
AppName: "streamable-http",
|
||||
AppSQN: scopes.QualifiedName{Name: "streamable-http"},
|
||||
},
|
||||
inputHeaderArgs: []string{
|
||||
"H1=v1",
|
||||
|
||||
@@ -48,6 +48,7 @@ import (
|
||||
libclient "github.com/gravitational/teleport/lib/client"
|
||||
"github.com/gravitational/teleport/lib/client/db/dbcmd"
|
||||
"github.com/gravitational/teleport/lib/defaults"
|
||||
"github.com/gravitational/teleport/lib/scopes"
|
||||
"github.com/gravitational/teleport/lib/srv/alpnproxy"
|
||||
alpncommon "github.com/gravitational/teleport/lib/srv/alpnproxy/common"
|
||||
"github.com/gravitational/teleport/lib/tlsca"
|
||||
@@ -613,9 +614,6 @@ func alpnProtocolForApp(app types.Application, appHTTPSTunnel bool) (alpncommon.
|
||||
}
|
||||
|
||||
func onProxyCommandApp(cf *CLIConf) error {
|
||||
if err := parseScopeQualifiedAppName(cf); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
tc, err := makeClient(cf)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
@@ -671,7 +669,7 @@ func onProxyCommandApp(cf *CLIConf) error {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
appName := cf.AppName
|
||||
appName := cf.AppSQN.Name
|
||||
if portMapping.TargetPort != 0 {
|
||||
appName = net.JoinHostPort(appName, strconv.Itoa(portMapping.TargetPort))
|
||||
}
|
||||
@@ -874,13 +872,13 @@ func onProxyCommandGCloud(cf *CLIConf) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadAppCertificate(tc *libclient.TeleportClient, appName string) (tls.Certificate, error) {
|
||||
func loadAppCertificate(tc *libclient.TeleportClient, appName, scope string) (tls.Certificate, error) {
|
||||
keyRing, err := tc.LocalAgent().GetKeyRing(tc.SiteName, libclient.WithAppCerts{})
|
||||
if err != nil {
|
||||
return tls.Certificate{}, trace.Wrap(err)
|
||||
}
|
||||
|
||||
appCert, err := keyRing.AppTLSCert(appName)
|
||||
appCert, err := keyRing.AppTLSCert(scopes.QualifiedName{Name: appName, Scope: scope})
|
||||
if trace.IsNotFound(err) {
|
||||
return tls.Certificate{}, trace.NotFound("please login into the application first: 'tsh apps login %v'", appName)
|
||||
} else if err != nil {
|
||||
|
||||
+15
-20
@@ -280,13 +280,8 @@ type CLIConf struct {
|
||||
DatabaseRoles string
|
||||
// DatabaseCommand specifies the command to execute.
|
||||
DatabaseCommand string
|
||||
|
||||
// AppName specifies proxied application name.
|
||||
AppName string
|
||||
// AppScope is the scope of the app to log into, parsed from a scope-qualified
|
||||
// app name argument ("/scope::name"). Scoped apps must be referenced by their
|
||||
// scope-qualified name; bare names refer to unscoped apps.
|
||||
AppScope string
|
||||
// AppSQN specifies proxied application name and scope.
|
||||
AppSQN scopes.QualifiedName
|
||||
// AppHTTPSTunnel enables a special mode to tunnel https for HTTP apps.
|
||||
// Mainly used for debugging purpose so the flag should be hidden.
|
||||
AppHTTPSTunnel bool
|
||||
@@ -1088,7 +1083,7 @@ func Run(ctx context.Context, args []string, opts ...CliOption) error {
|
||||
// Use Interspersed(false) to forward all flags to AWS CLI.
|
||||
aws := app.Command("aws", "Access AWS API.").Interspersed(false)
|
||||
aws.Arg("command", "AWS command and subcommands arguments that are going to be forwarded to AWS CLI.").StringsVar(&cf.AWSCommandArgs)
|
||||
aws.Flag("app", "Optional Name of the AWS application to use if logged into multiple.").StringVar(&cf.AppName)
|
||||
aws.Flag("app", "Optional Name of the AWS application to use if logged into multiple.").SetValue(&cf.AppSQN)
|
||||
aws.Flag("endpoint-url", "Run local proxy to serve as an AWS endpoint URL. If not specified, local proxy serves as an HTTPS proxy.").
|
||||
Short('e').Hidden().BoolVar(&cf.AWSEndpointURLMode)
|
||||
aws.Flag("exec", "Execute different commands (e.g. terraform) under Teleport credentials.").StringVar(&cf.Exec)
|
||||
@@ -1101,18 +1096,18 @@ func Run(ctx context.Context, args []string, opts ...CliOption) error {
|
||||
|
||||
azure := app.Command("az", "Access Azure API.").Interspersed(false)
|
||||
azure.Arg("command", "`az` command and subcommands arguments that are going to be forwarded to Azure CLI.").StringsVar(&cf.AzureCommandArgs)
|
||||
azure.Flag("app", "Optional name of the Azure application to use if logged into multiple.").StringVar(&cf.AppName)
|
||||
azure.Flag("app", "Optional name of the Azure application to use if logged into multiple.").SetValue(&cf.AppSQN)
|
||||
azure.Flag("azure-identity", "(For Azure CLI access only) Azure managed identity name.").StringVar(&cf.AzureIdentity)
|
||||
|
||||
gcloud := app.Command("gcloud", "Access GCP API with the gcloud command.").Interspersed(false)
|
||||
gcloud.Arg("command", "`gcloud` command and subcommands arguments.").StringsVar(&cf.GCPCommandArgs)
|
||||
gcloud.Flag("app", "Optional name of the GCP application to use if logged into multiple.").StringVar(&cf.AppName)
|
||||
gcloud.Flag("app", "Optional name of the GCP application to use if logged into multiple.").SetValue(&cf.AppSQN)
|
||||
gcloud.Flag("gcp-service-account", "(For GCP CLI access only) GCP service account name.").StringVar(&cf.GCPServiceAccount)
|
||||
gcloud.Alias("gcp")
|
||||
|
||||
gsutil := app.Command("gsutil", "Access Google Cloud Storage with the gsutil command.").Interspersed(false)
|
||||
gsutil.Arg("command", "`gsutil` command and subcommands arguments.").StringsVar(&cf.GCPCommandArgs)
|
||||
gsutil.Flag("app", "Optional name of the GCP application to use if logged into multiple.").StringVar(&cf.AppName)
|
||||
gsutil.Flag("app", "Optional name of the GCP application to use if logged into multiple.").SetValue(&cf.AppSQN)
|
||||
gsutil.Flag("gcp-service-account", "(For GCP CLI access only) GCP service account name.").StringVar(&cf.GCPServiceAccount)
|
||||
|
||||
// Applications.
|
||||
@@ -1126,7 +1121,7 @@ func Run(ctx context.Context, args []string, opts ...CliOption) error {
|
||||
lsApps.Arg("labels", labelHelp).StringVar(&cf.Labels)
|
||||
lsApps.Flag("all", "List apps from all clusters and proxies.").Short('R').BoolVar(&cf.ListAll)
|
||||
appLogin := apps.Command("login", "Retrieve short-lived certificate for an app.")
|
||||
appLogin.Arg("app", "App name to retrieve credentials for, as shown in `tsh apps ls`. Scoped apps use their scope-qualified name (\"/scope::name\").").Required().StringVar(&cf.AppName)
|
||||
appLogin.Arg("app", "App name to retrieve credentials for, as shown in `tsh apps ls`. Scoped apps use their scope-qualified name (\"/scope::name\").").Required().SetValue(&cf.AppSQN)
|
||||
appLogin.Flag("aws-role", "(For AWS CLI access only) Amazon IAM role ARN or role name.").StringVar(&cf.AWSRole)
|
||||
appLogin.Flag("env", "(For AWS CLI access only) Obtain credentials as plain text in order to load into environments variables. Required when using per-session MFA.").Hidden().BoolVar(&cf.AppLoginAWSEnvOutput)
|
||||
appLogin.Flag("azure-identity", "(For Azure CLI access only) Azure managed identity name.").StringVar(&cf.AzureIdentity)
|
||||
@@ -1135,14 +1130,14 @@ func Run(ctx context.Context, args []string, opts ...CliOption) error {
|
||||
appLogin.Flag("interactive", "Prompt for AWS Roles interactively (--aws-role takes precedence).").Short('T').Default("true").Envar(awsLoginInteractive).BoolVar(&cf.Interactive)
|
||||
appLogin.Flag("quiet", quietHelp).Short('q').BoolVar(&cf.Quiet)
|
||||
appLogout := apps.Command("logout", "Remove app certificate.")
|
||||
appLogout.Arg("app", "App to remove credentials for.").StringVar(&cf.AppName)
|
||||
appLogout.Arg("app", "App to remove credentials for.").SetValue(&cf.AppSQN)
|
||||
appConfig := apps.Command("config", "Print app connection information.")
|
||||
appConfig.Arg("app", "App to print information for. Required when logged into multiple apps.").StringVar(&cf.AppName)
|
||||
appConfig.Arg("app", "App to print information for. Required when logged into multiple apps.").SetValue(&cf.AppSQN)
|
||||
appConfig.Flag("format", fmt.Sprintf("Optional print format, one of: %q to print app address, %q to print CA cert path, %q to print cert path, %q print key path, %q to print example curl command, %q or %q to print everything as JSON or YAML.",
|
||||
appFormatURI, appFormatCA, appFormatCert, appFormatKey, appFormatCURL, appFormatJSON, appFormatYAML),
|
||||
).Short('f').StringVar(&cf.Format)
|
||||
appLogins := apps.Command("logins", "List available logins for a Cloud console application.")
|
||||
appLogins.Arg("app", "App name to list logins for. Currently only AWS is supported.").Required().StringVar(&cf.AppName)
|
||||
appLogins.Arg("app", "App name to list logins for. Currently only AWS is supported.").Required().SetValue(&cf.AppSQN)
|
||||
appLogins.Flag("format", defaults.FormatFlagDescription(defaults.DefaultFormats...)).Short('f').Default(teleport.Text).EnumVar(&cf.Format, defaults.DefaultFormats...)
|
||||
|
||||
// Recordings.
|
||||
@@ -1184,30 +1179,30 @@ func Run(ctx context.Context, args []string, opts ...CliOption) error {
|
||||
proxyDB.Flag("disable-access-request", "Disable automatic resource Access Requests.").BoolVar(&cf.disableAccessRequest)
|
||||
|
||||
proxyApp := proxy.Command("app", "Start local TLS proxy for app connection when using Teleport in single-port mode.")
|
||||
proxyApp.Arg("app", "The name of the application to start local proxy for.").Required().StringVar(&cf.AppName)
|
||||
proxyApp.Arg("app", "The name of the application to start local proxy for.").Required().SetValue(&cf.AppSQN)
|
||||
proxyApp.Flag("port", "Specifies the listening port used by the proxy app listener. Accepts an optional target port of a multi-port TCP app after a colon, e.g. \"1234:5678\".").Short('p').StringVar(&cf.LocalProxyPortMapping)
|
||||
proxyApp.Flag("cluster", clusterHelp).Short('c').StringVar(&cf.SiteName)
|
||||
proxyApp.Flag("https-tunnel", "Use the teleport-app-https ALPN protocol (HTTPS tunneled over mTLS) for HTTP apps.").Hidden().BoolVar(&cf.AppHTTPSTunnel)
|
||||
|
||||
proxyMCP := proxy.Command("mcp", "Start local proxy for MCP access.")
|
||||
proxyMCP.Arg("app", "The name of the MCP application to start local proxy for.").Required().StringVar(&cf.AppName)
|
||||
proxyMCP.Arg("app", "The name of the MCP application to start local proxy for.").Required().SetValue(&cf.AppSQN)
|
||||
proxyMCP.Flag("port", "Specifies the listening port used by the proxy app listener.").Short('p').StringVar(&cf.LocalProxyPortMapping)
|
||||
proxyMCP.Flag("cluster", clusterHelp).Short('c').StringVar(&cf.SiteName)
|
||||
|
||||
proxyAWS := proxy.Command("aws", "Start local proxy for AWS access.")
|
||||
proxyAWS.Flag("app", "Optional Name of the AWS application to use if logged into multiple.").StringVar(&cf.AppName)
|
||||
proxyAWS.Flag("app", "Optional Name of the AWS application to use if logged into multiple.").SetValue(&cf.AppSQN)
|
||||
proxyAWS.Flag("port", "Specifies the source port used by the proxy listener.").Short('p').StringVar(&cf.LocalProxyPort)
|
||||
proxyAWS.Flag("endpoint-url", "Run local proxy to serve as an AWS endpoint URL. If not specified, local proxy serves as an HTTPS proxy.").Short('e').Hidden().BoolVar(&cf.AWSEndpointURLMode)
|
||||
proxyAWS.Flag("format", awsProxyFormatFlagDescription()).Short('f').Default(envVarDefaultFormat()).EnumVar(&cf.Format, awsProxyFormats...)
|
||||
|
||||
proxyAzure := proxy.Command("azure", "Start local proxy for Azure access.")
|
||||
proxyAzure.Flag("app", "Optional Name of the Azure application to use if logged into multiple.").StringVar(&cf.AppName)
|
||||
proxyAzure.Flag("app", "Optional Name of the Azure application to use if logged into multiple.").SetValue(&cf.AppSQN)
|
||||
proxyAzure.Flag("port", "Specifies the source port used by the proxy listener.").Short('p').StringVar(&cf.LocalProxyPort)
|
||||
proxyAzure.Flag("format", envVarFormatFlagDescription()).Short('f').Default(envVarDefaultFormat()).EnumVar(&cf.Format, envVarFormats...)
|
||||
proxyAzure.Alias("az")
|
||||
|
||||
proxyGcloud := proxy.Command("gcloud", "Start local proxy for GCP access.")
|
||||
proxyGcloud.Flag("app", "Optional Name of the GCP application to use if logged into multiple.").StringVar(&cf.AppName)
|
||||
proxyGcloud.Flag("app", "Optional Name of the GCP application to use if logged into multiple.").SetValue(&cf.AppSQN)
|
||||
proxyGcloud.Flag("port", "Specifies the source port used by the proxy listener.").Short('p').StringVar(&cf.LocalProxyPort)
|
||||
proxyGcloud.Flag("format", envVarFormatFlagDescription()).Short('f').Default(envVarDefaultFormat()).EnumVar(&cf.Format, envVarFormats...)
|
||||
proxyGcloud.Alias("gcp")
|
||||
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
"github.com/gravitational/teleport/lib/client"
|
||||
"github.com/gravitational/teleport/lib/client/clientcache"
|
||||
libhwk "github.com/gravitational/teleport/lib/hardwarekey"
|
||||
"github.com/gravitational/teleport/lib/scopes"
|
||||
"github.com/gravitational/teleport/lib/utils"
|
||||
"github.com/gravitational/teleport/lib/vnet"
|
||||
)
|
||||
@@ -297,7 +298,7 @@ func (p *vnetClientApplication) reissueAppCert(ctx context.Context, tc *client.T
|
||||
return tls.Certificate{}, trace.Wrap(err, "logging in to app")
|
||||
}
|
||||
|
||||
cert, err := keyRing.AppTLSCert(routeToApp.Name)
|
||||
cert, err := keyRing.AppTLSCert(scopes.QualifiedName{Name: routeToApp.Name, Scope: routeToApp.Scope})
|
||||
if err != nil {
|
||||
return tls.Certificate{}, trace.Wrap(err, "getting TLS cert from key")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user