mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-24 16:17:11 +08:00
This commit fixes #3369, refs #3374 It adds support for kuberenetes_users section in roles, allowing Teleport proxy to impersonate user identities. It also extends variable interpolation syntax by adding suffix and prefix to variables and function `email.local`: Example: ```yaml kind: role version: v3 metadata: name: admin spec: allow: # extract email local part from the email claim logins: ['{{email.local(external.email)}}'] # impersonate a kubernetes user with IAM prefix kubernetes_users: ['IAM#{{external.email}}'] # the deny section uses the identical format as the 'allow' section. # the deny rules always override allow rules. deny: {} ``` Some notes on email.local behavior: * This is the only function supported in the template variables for now * In case if the email.local will encounter invalid email address, it will interpolate to empty value, will be removed from resulting output. Changes in impersonation behavior: * By default, if no kubernetes_users is set, which is a majority of cases, user will impersonate themselves, which is the backwards-compatible behavior. * As long as at least one `kubernetes_users` is set, the forwarder will start limiting the list of users allowed by the client to impersonate. * If the users' role set does not include actual user name, it will be rejected, otherwise there will be no way to exclude the user from the list). * If the `kuberentes_users` role set includes only one user (quite frequently that's the real intent), teleport will default to it, otherwise it will refuse to select. This will enable the use case when `kubernetes_users` has just one field to link the user identity with the IAM role, for example `IAM#{{external.email}}` * Previous versions of the forwarding proxy were denying all external impersonation headers, this commit allows 'Impesrsonate-User' and 'Impersonate-Group' header values that are allowed by role set. * Previous versions of the forwarding proxy ignored 'Deny' section of the roles when applied to impersonation, this commit fixes that - roles with deny kubernetes_users and kubernetes_groups section will not allow impersonation of those users and groups.
This commit is contained in:
@@ -445,6 +445,10 @@ const (
|
||||
// allowed kubernetes groups
|
||||
TraitKubeGroups = "kubernetes_groups"
|
||||
|
||||
// TraitKubeUsers is the name the role variable used to store
|
||||
// allowed kubernetes users
|
||||
TraitKubeUsers = "kubernetes_users"
|
||||
|
||||
// TraitInternalLoginsVariable is the variable used to store allowed
|
||||
// logins for local accounts.
|
||||
TraitInternalLoginsVariable = "{{internal.logins}}"
|
||||
|
||||
+1
-1
Submodule e updated: 1e87639954...0b124e0452
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2016-2019 Gravitational, Inc.
|
||||
Copyright 2016-2020 Gravitational, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -155,6 +155,7 @@ func (s *KubeSuite) TestKubeExec(c *check.C) {
|
||||
Allow: services.RoleConditions{
|
||||
Logins: []string{username},
|
||||
KubeGroups: []string{teleport.KubeSystemMasters},
|
||||
KubeUsers: []string{"alice@example.com"},
|
||||
},
|
||||
})
|
||||
t.AddUserWithRole(username, role)
|
||||
@@ -166,7 +167,8 @@ func (s *KubeSuite) TestKubeExec(c *check.C) {
|
||||
c.Assert(err, check.IsNil)
|
||||
defer t.Stop(true)
|
||||
|
||||
// impersonating client requests will be denied
|
||||
// impersonating client requests will be denied if the headers
|
||||
// are referencing users or groups not allowed by the existing roles
|
||||
impersonatingProxyClient, impersonatingProxyClientConfig, err := kubeProxyClient(kubeProxyConfig{
|
||||
t: t,
|
||||
username: username,
|
||||
@@ -180,6 +182,23 @@ func (s *KubeSuite) TestKubeExec(c *check.C) {
|
||||
})
|
||||
c.Assert(err, check.NotNil)
|
||||
|
||||
// scoped client requests will be allowed, as long as the impersonation headers
|
||||
// are referencing users and groups allowed by existing roles
|
||||
scopedProxyClient, scopedProxyClientConfig, err := kubeProxyClient(kubeProxyConfig{
|
||||
t: t,
|
||||
username: username,
|
||||
impersonation: &rest.ImpersonationConfig{
|
||||
UserName: role.GetKubeUsers(services.Allow)[0],
|
||||
Groups: role.GetKubeGroups(services.Allow),
|
||||
},
|
||||
})
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
_, err = scopedProxyClient.CoreV1().Pods(kubeSystemNamespace).List(metav1.ListOptions{
|
||||
LabelSelector: kubeDNSLabels.AsSelector().String(),
|
||||
})
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
// set up kube configuration using proxy
|
||||
proxyClient, proxyClientConfig, err := kubeProxyClient(kubeProxyConfig{t: t, username: username})
|
||||
c.Assert(err, check.IsNil)
|
||||
@@ -266,6 +285,68 @@ loop:
|
||||
})
|
||||
c.Assert(err, check.NotNil)
|
||||
c.Assert(err.Error(), check.Matches, ".*impersonation request has been denied.*")
|
||||
|
||||
// scoped kube exec is allowed, impersonation headers
|
||||
// are allowed by the role
|
||||
term = NewTerminal(250)
|
||||
term.Type("\aecho hi\n\r\aexit\n\r\a")
|
||||
out = &bytes.Buffer{}
|
||||
err = kubeExec(scopedProxyClientConfig, kubeExecArgs{
|
||||
podName: pod.Name,
|
||||
podNamespace: pod.Namespace,
|
||||
container: kubeDNSContainer,
|
||||
command: []string{"/bin/sh"},
|
||||
stdout: out,
|
||||
tty: true,
|
||||
stdin: &term,
|
||||
})
|
||||
c.Assert(err, check.IsNil)
|
||||
}
|
||||
|
||||
// TestKubeDeny makes sure that deny rule conflicting with allow
|
||||
// rule takes precendence
|
||||
func (s *KubeSuite) TestKubeDeny(c *check.C) {
|
||||
tconf := s.teleKubeConfig(Host)
|
||||
|
||||
t := NewInstance(InstanceConfig{
|
||||
ClusterName: Site,
|
||||
HostID: HostID,
|
||||
NodeName: Host,
|
||||
Ports: s.ports.PopIntSlice(5),
|
||||
Priv: s.priv,
|
||||
Pub: s.pub,
|
||||
})
|
||||
|
||||
username := s.me.Username
|
||||
role, err := services.NewRole("kubemaster", services.RoleSpecV3{
|
||||
Allow: services.RoleConditions{
|
||||
Logins: []string{username},
|
||||
KubeGroups: []string{teleport.KubeSystemMasters},
|
||||
KubeUsers: []string{"alice@example.com"},
|
||||
},
|
||||
Deny: services.RoleConditions{
|
||||
KubeGroups: []string{teleport.KubeSystemMasters},
|
||||
KubeUsers: []string{"alice@example.com"},
|
||||
},
|
||||
})
|
||||
t.AddUserWithRole(username, role)
|
||||
|
||||
err = t.CreateEx(nil, tconf)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
err = t.Start()
|
||||
c.Assert(err, check.IsNil)
|
||||
defer t.Stop(true)
|
||||
|
||||
// set up kube configuration using proxy
|
||||
proxyClient, _, err := kubeProxyClient(kubeProxyConfig{t: t, username: username})
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
// try get request to fetch available pods
|
||||
_, err = proxyClient.CoreV1().Pods(kubeSystemNamespace).List(metav1.ListOptions{
|
||||
LabelSelector: kubeDNSLabels.AsSelector().String(),
|
||||
})
|
||||
c.Assert(err, check.NotNil)
|
||||
}
|
||||
|
||||
// TestKubePortForward tests kubernetes port forwarding
|
||||
|
||||
+7
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2017 Gravitational, Inc.
|
||||
Copyright 2017-2020 Gravitational, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -289,9 +289,12 @@ type createUserParams struct {
|
||||
// logins is the list of *nix logins.
|
||||
logins []string
|
||||
|
||||
// kubeGroups is the list of Kubernetes this user belongs to.
|
||||
// kubeGroups is the list of Kubernetes groups this user belongs to.
|
||||
kubeGroups []string
|
||||
|
||||
// kubeUsers is the list of Kubernetes users this user belongs to.
|
||||
kubeUsers []string
|
||||
|
||||
// roles is the list of roles this user is assigned to.
|
||||
roles []string
|
||||
|
||||
@@ -309,14 +312,14 @@ func (s *AuthServer) calculateGithubUser(connector services.GithubConnector, cla
|
||||
}
|
||||
|
||||
// Calculate logins, kubegroups, roles, and traits.
|
||||
p.logins, p.kubeGroups = connector.MapClaims(*claims)
|
||||
p.logins, p.kubeGroups, p.kubeUsers = connector.MapClaims(*claims)
|
||||
if len(p.logins) == 0 {
|
||||
return nil, trace.BadParameter(
|
||||
"user %q does not belong to any teams configured in %q connector",
|
||||
claims.Username, connector.GetName())
|
||||
}
|
||||
p.roles = modules.GetModules().RolesFromLogins(p.logins)
|
||||
p.traits = modules.GetModules().TraitsFromLogins(p.logins, p.kubeGroups)
|
||||
p.traits = modules.GetModules().TraitsFromLogins(p.logins, p.kubeGroups, p.kubeUsers)
|
||||
|
||||
// Pick smaller for role: session TTL from role or requested TTL.
|
||||
roles, err := services.FetchRoles(p.roles, s.Access, p.traits)
|
||||
|
||||
+2
-1
@@ -98,7 +98,7 @@ func (s *AuthServer) ProcessKubeCSR(req KubeCSR) (*KubeCSRResponse, error) {
|
||||
|
||||
// extract and encode the kubernetes groups of the authenticated
|
||||
// user in the newly issued certificate
|
||||
kubernetesGroups, err := roles.CheckKubeGroups(0)
|
||||
kubernetesGroups, kubernetesUsers, err := roles.CheckKubeGroupsAndUsers(0)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
@@ -129,6 +129,7 @@ func (s *AuthServer) ProcessKubeCSR(req KubeCSR) (*KubeCSRResponse, error) {
|
||||
// otherwise proxies can generate certs for any user.
|
||||
Usage: []string{teleport.UsageKubeOnly},
|
||||
KubernetesGroups: kubernetesGroups,
|
||||
KubernetesUsers: kubernetesUsers,
|
||||
}
|
||||
subject, err := identity.Subject()
|
||||
if err != nil {
|
||||
|
||||
@@ -261,6 +261,7 @@ func (a *AuthMiddleware) GetUser(r *http.Request) (IdentityGetter, error) {
|
||||
Username: identity.Username,
|
||||
Principals: identity.Principals,
|
||||
KubernetesGroups: identity.KubernetesGroups,
|
||||
KubernetesUsers: identity.KubernetesUsers,
|
||||
RemoteRoles: identity.Groups,
|
||||
Identity: *identity,
|
||||
}, nil
|
||||
|
||||
@@ -147,15 +147,17 @@ func (a *authorizer) authorizeRemoteUser(u RemoteUser) (*AuthContext, error) {
|
||||
return nil, trace.AccessDenied("no roles mapped for remote user %q from cluster %q", u.Username, u.ClusterName)
|
||||
}
|
||||
// Set "logins" trait and "kubernetes_groups" for the remote user. This allows Teleport to work by
|
||||
// passing exact logins and kubernetes groups to the remote cluster. Note that claims (OIDC/SAML)
|
||||
// passing exact logins, kubernetes groups and users to the remote cluster. Note that claims (OIDC/SAML)
|
||||
// are not passed, but rather the exact logins, this is done to prevent
|
||||
// leaking too much of identity to the remote cluster, and instead of focus
|
||||
// on main cluster's interpretation of this identity
|
||||
traits := map[string][]string{
|
||||
teleport.TraitLogins: u.Principals,
|
||||
teleport.TraitKubeGroups: u.KubernetesGroups,
|
||||
teleport.TraitKubeUsers: u.KubernetesUsers,
|
||||
}
|
||||
log.Debugf("Mapped roles %v of remote user %q to local roles %v and traits %v.", u.RemoteRoles, u.Username, roleNames, traits)
|
||||
log.Debugf("Mapped roles %v of remote user %q to local roles %v and traits %v.",
|
||||
u.RemoteRoles, u.Username, roleNames, traits)
|
||||
checker, err := services.FetchRoles(roleNames, a.access, traits)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
@@ -589,6 +591,9 @@ type RemoteUser struct {
|
||||
// KubernetesGroups is a list of Kubernetes groups
|
||||
KubernetesGroups []string `json:"kubernetes_groups"`
|
||||
|
||||
// KubernetesUsers is a list of Kubernetes users
|
||||
KubernetesUsers []string `json:"kubernetes_users"`
|
||||
|
||||
// Identity is source x509 used to build this role
|
||||
Identity tlsca.Identity
|
||||
}
|
||||
|
||||
+110
-20
@@ -221,7 +221,8 @@ func (f *Forwarder) Close() error {
|
||||
// contains information about user, target cluster and authenticated groups
|
||||
type authContext struct {
|
||||
auth.AuthContext
|
||||
kubeGroups []string
|
||||
kubeGroups map[string]struct{}
|
||||
kubeUsers map[string]struct{}
|
||||
cluster cluster
|
||||
clusterConfig services.ClusterConfig
|
||||
// clientIdleTimeout sets information on client idle timeout
|
||||
@@ -234,13 +235,13 @@ type authContext struct {
|
||||
}
|
||||
|
||||
func (c authContext) String() string {
|
||||
return fmt.Sprintf("user: %v, groups: %v, cluster: %v", c.User.GetName(), c.kubeGroups, c.cluster.GetName())
|
||||
return fmt.Sprintf("user: %v, users: %v, groups: %v, cluster: %v", c.User.GetName(), c.kubeUsers, c.kubeGroups, c.cluster.GetName())
|
||||
}
|
||||
|
||||
func (c *authContext) key() string {
|
||||
// it is important that the context key contains user, kubernetes groups and certificate expiry,
|
||||
// so that new logins with different parameters will not reuse this context
|
||||
return fmt.Sprintf("%v:%v:%v:%v", c.cluster.GetName(), c.User.GetName(), c.kubeGroups, c.disconnectExpiredCert.UTC().Unix())
|
||||
return fmt.Sprintf("%v:%v:%v:%v:%v", c.cluster.GetName(), c.User.GetName(), c.kubeUsers, c.kubeGroups, c.disconnectExpiredCert.UTC().Unix())
|
||||
}
|
||||
|
||||
// cluster represents cluster information, name of the cluster
|
||||
@@ -357,11 +358,17 @@ func (f *Forwarder) setupContext(ctx auth.AuthContext, req *http.Request, isRemo
|
||||
sessionTTL := roles.AdjustSessionTTL(time.Hour)
|
||||
|
||||
// check signing TTL and return a list of allowed logins
|
||||
kubeGroups, err := roles.CheckKubeGroups(sessionTTL)
|
||||
kubeGroups, kubeUsers, err := roles.CheckKubeGroupsAndUsers(sessionTTL)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// By default, if no kubernetes_users is set (which will be a majority),
|
||||
// user will impersonate themselves, which is the backwards-compatible behavior.
|
||||
if len(kubeUsers) == 0 {
|
||||
kubeUsers = append(kubeUsers, ctx.User.GetName())
|
||||
}
|
||||
|
||||
// KubeSystemAuthenticated is a builtin group that allows
|
||||
// any user to access common API methods, e.g. discovery methods
|
||||
// required for initial client usage, without it, restricted user's
|
||||
@@ -376,7 +383,6 @@ func (f *Forwarder) setupContext(ctx auth.AuthContext, req *http.Request, isRemo
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
if ctx.Identity.RouteToCluster != "" {
|
||||
f.Debugf("Client certificate of %v has requested routing to a cluster: %v.", ctx.User.GetName(), ctx.Identity.RouteToCluster)
|
||||
targetCluster, err = f.Tunnel.GetSite(ctx.Identity.RouteToCluster)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
@@ -411,7 +417,8 @@ func (f *Forwarder) setupContext(ctx auth.AuthContext, req *http.Request, isRemo
|
||||
clientIdleTimeout: roles.AdjustClientIdleTimeout(clusterConfig.GetClientIdleTimeout()),
|
||||
sessionTTL: sessionTTL,
|
||||
AuthContext: ctx,
|
||||
kubeGroups: kubeGroups,
|
||||
kubeGroups: utils.StringsSet(kubeGroups),
|
||||
kubeUsers: utils.StringsSet(kubeUsers),
|
||||
clusterConfig: clusterConfig,
|
||||
cluster: cluster{
|
||||
remoteAddr: utils.NetAddr{AddrNetwork: "tcp", Addr: req.RemoteAddr},
|
||||
@@ -634,11 +641,21 @@ func (f *Forwarder) portForward(ctx *authContext, w http.ResponseWriter, req *ht
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
const (
|
||||
// ImpersonateHeaderPrefix is K8s impersonation prefix for impersonation feature:
|
||||
// https://kubernetes.io/docs/reference/access-authn-authz/authentication/#user-impersonation
|
||||
ImpersonateHeaderPrefix = "Impersonate-"
|
||||
// ImpersonateUserHeader is impersonation header for users
|
||||
ImpersonateUserHeader = "Impersonate-User"
|
||||
// ImpersonateGroupHeader is K8s impersonation header for user
|
||||
ImpersonateGroupHeader = "Impersonate-Group"
|
||||
// ImpersonationRequestDeniedMessage is access denied message for impersonation
|
||||
ImpersonationRequestDeniedMessage = "impersonation request has been denied"
|
||||
)
|
||||
|
||||
func (f *Forwarder) setupForwardingHeaders(ctx *authContext, sess *clusterSession, req *http.Request) error {
|
||||
for header := range req.Header {
|
||||
if strings.HasPrefix(header, "Impersonate-") {
|
||||
return trace.AccessDenied("impersonation request has been denied")
|
||||
}
|
||||
if err := setupImpersonationHeaders(f.Entry, ctx, req.Header, f.creds.cfg.BearerToken); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
// Setup scheme, override target URL to the destination address
|
||||
@@ -652,16 +669,89 @@ func (f *Forwarder) setupForwardingHeaders(ctx *authContext, sess *clusterSessio
|
||||
req.Header.Add("X-Forwarded-Host", req.Host)
|
||||
req.Header.Add("X-Forwarded-Path", req.URL.Path)
|
||||
|
||||
if !ctx.cluster.isRemote {
|
||||
req.Header.Add("Impersonate-User", ctx.User.GetName())
|
||||
f.Debugf("Impersonate User: %v", ctx.User)
|
||||
for _, group := range ctx.kubeGroups {
|
||||
req.Header.Add("Impersonate-Group", group)
|
||||
f.Debugf("Impersonate Group: %v", group)
|
||||
return nil
|
||||
}
|
||||
|
||||
// setupImpersonationHeaders sets up Impersonate-User and Impersonate-Group headers
|
||||
func setupImpersonationHeaders(log log.FieldLogger, ctx *authContext, headers http.Header, bearerToken string) error {
|
||||
var impersonateUser string
|
||||
var impersonateGroups []string
|
||||
for header, values := range headers {
|
||||
if !strings.HasPrefix(header, "Impersonate-") {
|
||||
continue
|
||||
}
|
||||
if f.creds.cfg.BearerToken != "" {
|
||||
f.Debugf("Using Bearer Token Auth")
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", f.creds.cfg.BearerToken))
|
||||
switch header {
|
||||
case ImpersonateUserHeader:
|
||||
if impersonateUser != "" {
|
||||
return trace.AccessDenied("%v, user already specified to %q", ImpersonationRequestDeniedMessage, impersonateUser)
|
||||
}
|
||||
if len(values) == 0 || len(values) > 1 {
|
||||
return trace.AccessDenied("%v, invalid user header %q", ImpersonationRequestDeniedMessage, values)
|
||||
}
|
||||
impersonateUser = values[0]
|
||||
if _, ok := ctx.kubeUsers[impersonateUser]; !ok {
|
||||
return trace.AccessDenied("%v, user header %q is not allowed in roles", ImpersonationRequestDeniedMessage, impersonateUser)
|
||||
}
|
||||
case ImpersonateGroupHeader:
|
||||
for _, group := range values {
|
||||
if _, ok := ctx.kubeGroups[group]; !ok {
|
||||
return trace.AccessDenied("%v, group header %q value is not allowed in roles", ImpersonationRequestDeniedMessage, group)
|
||||
}
|
||||
impersonateGroups = append(impersonateGroups, group)
|
||||
}
|
||||
default:
|
||||
return trace.AccessDenied("%v, unsupported impersonation header %q", ImpersonationRequestDeniedMessage, header)
|
||||
}
|
||||
}
|
||||
|
||||
impersonateGroups = utils.Deduplicate(impersonateGroups)
|
||||
|
||||
// By default, if no kubernetes_users is set (which will be a majority),
|
||||
// user will impersonate themselves, which is the backwards-compatible behavior.
|
||||
//
|
||||
// As long as at least one `kubernetes_users` is set, the forwarder will start
|
||||
// limiting the list of users allowed by the client to impersonate.
|
||||
//
|
||||
// If the users' role set does not include actual user name, it will be rejected,
|
||||
// otherwise there will be no way to exclude the user from the list).
|
||||
//
|
||||
// If the `kubernetes_users` role set includes only one user
|
||||
// (quite frequently that's the real intent), teleport will default to it,
|
||||
// otherwise it will refuse to select.
|
||||
//
|
||||
// This will enable the use case when `kubernetes_users` has just one field to
|
||||
// link the user identity with the IAM role, for example `IAM#{{external.email}}`
|
||||
//
|
||||
if impersonateUser == "" {
|
||||
switch len(ctx.kubeUsers) {
|
||||
// this is currently not possible as kube users have at least one
|
||||
// user (user name), but in case if someone breaks it, catch here
|
||||
case 0:
|
||||
return trace.AccessDenied("assumed at least one user to be present")
|
||||
// if there is deterministic choice, make it to improve user experience
|
||||
case 1:
|
||||
for user := range ctx.kubeUsers {
|
||||
impersonateUser = user
|
||||
break
|
||||
}
|
||||
default:
|
||||
return trace.AccessDenied(
|
||||
"please select a user to impersonate, refusing to select a user due to several kuberenetes_users set up for this user")
|
||||
}
|
||||
}
|
||||
if len(impersonateGroups) == 0 {
|
||||
for group := range ctx.kubeGroups {
|
||||
impersonateGroups = append(impersonateGroups, group)
|
||||
}
|
||||
}
|
||||
|
||||
if !ctx.cluster.isRemote {
|
||||
headers.Add("Impersonate-User", impersonateUser)
|
||||
for _, group := range impersonateGroups {
|
||||
headers.Add("Impersonate-Group", group)
|
||||
}
|
||||
if bearerToken != "" {
|
||||
headers.Set("Authorization", fmt.Sprintf("Bearer %v", bearerToken))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -993,7 +1083,7 @@ func (f *Forwarder) requestCertificate(ctx authContext) (*bundle, error) {
|
||||
csr := &x509.CertificateRequest{
|
||||
Subject: pkix.Name{
|
||||
CommonName: ctx.User.GetName(),
|
||||
Organization: ctx.kubeGroups,
|
||||
Organization: utils.StringsSliceFromSet(ctx.kubeGroups),
|
||||
},
|
||||
}
|
||||
csrBytes, err := x509.CreateCertificateRequest(rand.Reader, csr, privateKey)
|
||||
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
|
||||
"github.com/gravitational/teleport/lib/utils"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
log "github.com/sirupsen/logrus"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -189,18 +190,8 @@ func (s *SpdyRoundTripper) RoundTrip(req *http.Request) (*http.Response, error)
|
||||
header.Add(httpstream.HeaderConnection, httpstream.HeaderUpgrade)
|
||||
header.Add(httpstream.HeaderUpgrade, streamspdy.HeaderSpdy31)
|
||||
|
||||
// impersonation for remote clusters is handled by remote proxies
|
||||
if !s.authCtx.cluster.isRemote {
|
||||
header.Add("Impersonate-User", s.authCtx.User.GetName())
|
||||
log.Debugf("Impersonate User: %v", s.authCtx.User)
|
||||
for _, group := range s.authCtx.kubeGroups {
|
||||
header.Add("Impersonate-Group", group)
|
||||
log.Debugf("Impersonate Group: %v", group)
|
||||
}
|
||||
if s.bearerToken != "" {
|
||||
log.Debugf("Using Bearer Token Auth")
|
||||
header.Set("Authorization", fmt.Sprintf("Bearer %v", s.bearerToken))
|
||||
}
|
||||
if err := setupImpersonationHeaders(log.StandardLogger(), &s.authCtx, header, s.bearerToken); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
@@ -44,7 +44,7 @@ type Modules interface {
|
||||
RolesFromLogins([]string) []string
|
||||
// TraitsFromLogins returns traits for external user based on the logins
|
||||
// and kubernetes groups extracted from the connector
|
||||
TraitsFromLogins([]string, []string) map[string][]string
|
||||
TraitsFromLogins(logins []string, kubeGroups []string, kubeUsers []string) map[string][]string
|
||||
// SupportsKubernetes returns true if this cluster supports kubernetes
|
||||
SupportsKubernetes() bool
|
||||
// IsBoringBinary checks if the binary was compiled with BoringCrypto.
|
||||
@@ -107,10 +107,11 @@ func (p *defaultModules) RolesFromLogins(logins []string) []string {
|
||||
// extracted from the connector
|
||||
//
|
||||
// By default logins are treated as allowed logins user traits.
|
||||
func (p *defaultModules) TraitsFromLogins(logins []string, kubeGroups []string) map[string][]string {
|
||||
func (p *defaultModules) TraitsFromLogins(logins []string, kubeGroups, kubeUsers []string) map[string][]string {
|
||||
return map[string][]string{
|
||||
teleport.TraitLogins: logins,
|
||||
teleport.TraitKubeGroups: kubeGroups,
|
||||
teleport.TraitKubeUsers: kubeUsers,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,10 +44,11 @@ func (s *ModulesSuite) TestDefaultModules(c *check.C) {
|
||||
roles := GetModules().RolesFromLogins([]string{"root"})
|
||||
c.Assert(roles, check.DeepEquals, []string{teleport.AdminRoleName})
|
||||
|
||||
traits := GetModules().TraitsFromLogins([]string{"root"}, []string{"system:masters"})
|
||||
traits := GetModules().TraitsFromLogins([]string{"root"}, []string{"system:masters"}, []string{"alice@example.com"})
|
||||
c.Assert(traits, check.DeepEquals, map[string][]string{
|
||||
teleport.TraitLogins: []string{"root"},
|
||||
teleport.TraitKubeGroups: []string{"system:masters"},
|
||||
teleport.TraitKubeUsers: []string{"alice@example.com"},
|
||||
})
|
||||
|
||||
isBoring := GetModules().IsBoringBinary()
|
||||
@@ -66,7 +67,7 @@ func (s *ModulesSuite) TestTestModules(c *check.C) {
|
||||
roles := GetModules().RolesFromLogins([]string{"root"})
|
||||
c.Assert(roles, check.DeepEquals, []string{"root"})
|
||||
|
||||
traits := GetModules().TraitsFromLogins([]string{"root"}, []string{"system:masters"})
|
||||
traits := GetModules().TraitsFromLogins([]string{"root"}, []string{"system:masters"}, []string{"alice@example.com"})
|
||||
c.Assert(traits, check.IsNil)
|
||||
|
||||
isBoring := GetModules().IsBoringBinary()
|
||||
@@ -97,7 +98,7 @@ func (p *testModules) RolesFromLogins(logins []string) []string {
|
||||
return logins
|
||||
}
|
||||
|
||||
func (p *testModules) TraitsFromLogins(logins []string, kubeGroups []string) map[string][]string {
|
||||
func (p *testModules) TraitsFromLogins(logins []string, kubeGroups []string, kubeUsers []string) map[string][]string {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ type GithubConnector interface {
|
||||
SetTeamsToLogins([]TeamMapping)
|
||||
// MapClaims returns the list of allows logins based on the retrieved claims
|
||||
// returns list of logins and kubernetes groups
|
||||
MapClaims(GithubClaims) ([]string, []string)
|
||||
MapClaims(GithubClaims) (logins []string, kubeGroups []string, kubeUsers []string)
|
||||
// GetDisplay returns the connector display name
|
||||
GetDisplay() string
|
||||
// SetDisplay sets the connector display name
|
||||
@@ -110,6 +110,9 @@ type TeamMapping struct {
|
||||
Logins []string `json:"logins,omitempty"`
|
||||
// KubeGroups is a list of allowed kubernetes groups for this org/team
|
||||
KubeGroups []string `json:"kubernetes_groups,omitempty"`
|
||||
// KubeUsers is a list of allowed kubernetes users to impersonate for
|
||||
// this org/team
|
||||
KubeUsers []string `json:"kubernetes_users,omitempty"`
|
||||
}
|
||||
|
||||
// GithubClaims represents Github user information obtained during OAuth2 flow
|
||||
@@ -240,8 +243,8 @@ func (c *GithubConnectorV3) SetDisplay(display string) {
|
||||
|
||||
// MapClaims returns a list of logins based on the provided claims,
|
||||
// returns a list of logins and list of kubernetes groups
|
||||
func (c *GithubConnectorV3) MapClaims(claims GithubClaims) ([]string, []string) {
|
||||
var logins, kubeGroups []string
|
||||
func (c *GithubConnectorV3) MapClaims(claims GithubClaims) ([]string, []string, []string) {
|
||||
var logins, kubeGroups, kubeUsers []string
|
||||
for _, mapping := range c.GetTeamsToLogins() {
|
||||
teams, ok := claims.OrganizationToTeams[mapping.Organization]
|
||||
if !ok {
|
||||
@@ -253,10 +256,11 @@ func (c *GithubConnectorV3) MapClaims(claims GithubClaims) ([]string, []string)
|
||||
if team == mapping.Team {
|
||||
logins = append(logins, mapping.Logins...)
|
||||
kubeGroups = append(kubeGroups, mapping.KubeGroups...)
|
||||
kubeUsers = append(kubeUsers, mapping.KubeUsers...)
|
||||
}
|
||||
}
|
||||
}
|
||||
return utils.Deduplicate(logins), utils.Deduplicate(kubeGroups)
|
||||
return utils.Deduplicate(logins), utils.Deduplicate(kubeGroups), utils.Deduplicate(kubeUsers)
|
||||
}
|
||||
|
||||
var githubConnectorMarshaler GithubConnectorMarshaler = &TeleportGithubConnectorMarshaler{}
|
||||
|
||||
@@ -77,6 +77,7 @@ func (s *GithubSuite) TestMapClaims(c *check.C) {
|
||||
Team: "admins",
|
||||
Logins: []string{"admin", "dev"},
|
||||
KubeGroups: []string{"system:masters", "kube-devs"},
|
||||
KubeUsers: []string{"alice@example.com"},
|
||||
},
|
||||
{
|
||||
Organization: "gravitational",
|
||||
@@ -86,15 +87,16 @@ func (s *GithubSuite) TestMapClaims(c *check.C) {
|
||||
},
|
||||
},
|
||||
})
|
||||
logins, kubeGroups := connector.MapClaims(GithubClaims{
|
||||
logins, kubeGroups, kubeUsers := connector.MapClaims(GithubClaims{
|
||||
OrganizationToTeams: map[string][]string{
|
||||
"gravitational": []string{"admins"},
|
||||
},
|
||||
})
|
||||
c.Assert(logins, check.DeepEquals, []string{"admin", "dev"})
|
||||
c.Assert(kubeGroups, check.DeepEquals, []string{"system:masters", "kube-devs"})
|
||||
c.Assert(kubeUsers, check.DeepEquals, []string{"alice@example.com"})
|
||||
|
||||
logins, kubeGroups = connector.MapClaims(GithubClaims{
|
||||
logins, kubeGroups, kubeUsers = connector.MapClaims(GithubClaims{
|
||||
OrganizationToTeams: map[string][]string{
|
||||
"gravitational": []string{"devs"},
|
||||
},
|
||||
@@ -102,7 +104,7 @@ func (s *GithubSuite) TestMapClaims(c *check.C) {
|
||||
c.Assert(logins, check.DeepEquals, []string{"dev", "test"})
|
||||
c.Assert(kubeGroups, check.DeepEquals, []string{"kube-devs"})
|
||||
|
||||
logins, kubeGroups = connector.MapClaims(GithubClaims{
|
||||
logins, kubeGroups, kubeUsers = connector.MapClaims(GithubClaims{
|
||||
OrganizationToTeams: map[string][]string{
|
||||
"gravitational": []string{"admins", "devs"},
|
||||
},
|
||||
|
||||
+76
-28
@@ -272,6 +272,11 @@ type Role interface {
|
||||
// SetKubeGroups sets kubernetes groups for allow or deny condition.
|
||||
SetKubeGroups(RoleConditionType, []string)
|
||||
|
||||
// GetKubeUsers returns kubernetes users to impersonate
|
||||
GetKubeUsers(RoleConditionType) []string
|
||||
// SetKubeUsers sets kubernetes users to impersonate for allow or deny condition.
|
||||
SetKubeUsers(RoleConditionType, []string)
|
||||
|
||||
// GetAccessRequestConditions gets allow/deny conditions for access requests.
|
||||
GetAccessRequestConditions(RoleConditionType) AccessRequestConditions
|
||||
// SetAccessRequestConditions sets allow/deny conditions for access requests.
|
||||
@@ -323,6 +328,21 @@ func ApplyTraits(r Role, traits map[string][]string) Role {
|
||||
}
|
||||
r.SetKubeGroups(condition, utils.Deduplicate(outKubeGroups))
|
||||
|
||||
// apply templates to kubernetes users
|
||||
inKubeUsers := r.GetKubeUsers(condition)
|
||||
var outKubeUsers []string
|
||||
for _, user := range inKubeUsers {
|
||||
variableValues, err := applyValueTraits(user, traits)
|
||||
if err != nil {
|
||||
if !trace.IsNotFound(err) {
|
||||
log.Debugf("Skipping kube user %v: %v.", user, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
outKubeUsers = append(outKubeUsers, variableValues...)
|
||||
}
|
||||
r.SetKubeUsers(condition, utils.Deduplicate(outKubeUsers))
|
||||
|
||||
inLabels := r.GetNodeLabels(condition)
|
||||
// to avoid unnecessary allocations
|
||||
if inLabels != nil {
|
||||
@@ -362,9 +382,8 @@ func ApplyTraits(r Role, traits map[string][]string) Role {
|
||||
// mapped list of values otherwise, the function guarantees to return
|
||||
// at least one value in case if return value is nil
|
||||
func applyValueTraits(val string, traits map[string][]string) ([]string, error) {
|
||||
// Extract the variablePrefix and variableName from the role variable.
|
||||
variablePrefix, variableName, err := parse.IsRoleVariable(val)
|
||||
|
||||
// Extract the variable from the role variable.
|
||||
variable, err := parse.RoleVariable(val)
|
||||
if err != nil {
|
||||
if !trace.IsNotFound(err) {
|
||||
return nil, trace.Wrap(err)
|
||||
@@ -373,19 +392,21 @@ func applyValueTraits(val string, traits map[string][]string) ([]string, error)
|
||||
}
|
||||
|
||||
// For internal traits, only internal.logins and internal.kubernetes_groups is supported at the moment.
|
||||
if variablePrefix == teleport.TraitInternalPrefix {
|
||||
if variableName != teleport.TraitLogins && variableName != teleport.TraitKubeGroups {
|
||||
return nil, trace.BadParameter("unsupported variable %q", variableName)
|
||||
if variable.Namespace() == teleport.TraitInternalPrefix {
|
||||
if variable.Name() != teleport.TraitLogins && variable.Name() != teleport.TraitKubeGroups && variable.Name() != teleport.TraitKubeUsers {
|
||||
return nil, trace.BadParameter("unsupported variable %q", variable.Name())
|
||||
}
|
||||
}
|
||||
|
||||
// If the variable is not found in the traits, skip it.
|
||||
variableValues, ok := traits[variableName]
|
||||
if !ok || len(variableValues) == 0 {
|
||||
return nil, trace.NotFound("variable %q not found in traits", variableName)
|
||||
interpolated, err := variable.Interpolate(traits)
|
||||
if trace.IsNotFound(err) || len(interpolated) == 0 {
|
||||
return nil, trace.NotFound("variable %q not found in traits", variable.Name())
|
||||
}
|
||||
|
||||
return append([]string{}, variableValues...), nil
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
return interpolated, nil
|
||||
}
|
||||
|
||||
// GetVersion returns resource version
|
||||
@@ -527,6 +548,25 @@ func (r *RoleV3) SetKubeGroups(rct RoleConditionType, groups []string) {
|
||||
}
|
||||
}
|
||||
|
||||
// GetKubeUsers returns kubernetes users
|
||||
func (r *RoleV3) GetKubeUsers(rct RoleConditionType) []string {
|
||||
if rct == Allow {
|
||||
return r.Spec.Allow.KubeUsers
|
||||
}
|
||||
return r.Spec.Deny.KubeUsers
|
||||
}
|
||||
|
||||
// SetKubeUsers sets kubernetes user for allow or deny condition.
|
||||
func (r *RoleV3) SetKubeUsers(rct RoleConditionType, users []string) {
|
||||
lcopy := utils.CopyStrings(users)
|
||||
|
||||
if rct == Allow {
|
||||
r.Spec.Allow.KubeUsers = lcopy
|
||||
} else {
|
||||
r.Spec.Deny.KubeUsers = lcopy
|
||||
}
|
||||
}
|
||||
|
||||
// GetAccessRequestConditions gets conditions for access requests.
|
||||
func (r *RoleV3) GetAccessRequestConditions(rct RoleConditionType) AccessRequestConditions {
|
||||
cond := r.Spec.Deny.Request
|
||||
@@ -647,7 +687,7 @@ func (r *RoleV3) CheckAndSetDefaults() error {
|
||||
for _, condition := range []RoleConditionType{Allow, Deny} {
|
||||
for _, login := range r.GetLogins(condition) {
|
||||
if strings.Contains(login, "{{") || strings.Contains(login, "}}") {
|
||||
_, _, err := parse.IsRoleVariable(login)
|
||||
_, err := parse.RoleVariable(login)
|
||||
if err != nil {
|
||||
return trace.BadParameter("invalid login found: %v", login)
|
||||
}
|
||||
@@ -1285,9 +1325,9 @@ type AccessChecker interface {
|
||||
// returns a combined list of allowed logins.
|
||||
CheckLoginDuration(ttl time.Duration) ([]string, error)
|
||||
|
||||
// CheckKubeGroups check if role can login into kubernetes
|
||||
// and returns a combined list of allowed groups
|
||||
CheckKubeGroups(ttl time.Duration) ([]string, error)
|
||||
// CheckKubeGroupsAndUsers check if role can login into kubernetes
|
||||
// and returns two lists of combined allowed groups and users
|
||||
CheckKubeGroupsAndUsers(ttl time.Duration) (groups []string, users []string, err error)
|
||||
|
||||
// AdjustSessionTTL will reduce the requested ttl to lowest max allowed TTL
|
||||
// for this role set, otherwise it returns ttl unchanged
|
||||
@@ -1626,31 +1666,39 @@ func (set RoleSet) AdjustDisconnectExpiredCert(disconnect bool) bool {
|
||||
return disconnect
|
||||
}
|
||||
|
||||
// CheckKubeGroups check if role can login into kubernetes
|
||||
// and returns a combined list of allowed groups
|
||||
func (set RoleSet) CheckKubeGroups(ttl time.Duration) ([]string, error) {
|
||||
groups := make(map[string]bool)
|
||||
// CheckKubeGroupsAndUsers check if role can login into kubernetes
|
||||
// and returns two lists of allowed groups and users
|
||||
func (set RoleSet) CheckKubeGroupsAndUsers(ttl time.Duration) ([]string, []string, error) {
|
||||
groups := make(map[string]struct{})
|
||||
users := make(map[string]struct{})
|
||||
var matchedTTL bool
|
||||
for _, role := range set {
|
||||
maxSessionTTL := role.GetOptions().MaxSessionTTL.Value()
|
||||
if ttl <= maxSessionTTL && maxSessionTTL != 0 {
|
||||
matchedTTL = true
|
||||
for _, group := range role.GetKubeGroups(Allow) {
|
||||
groups[group] = true
|
||||
groups[group] = struct{}{}
|
||||
}
|
||||
for _, user := range role.GetKubeUsers(Allow) {
|
||||
users[user] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, role := range set {
|
||||
for _, group := range role.GetKubeGroups(Deny) {
|
||||
delete(groups, group)
|
||||
}
|
||||
for _, user := range role.GetKubeUsers(Deny) {
|
||||
delete(users, user)
|
||||
}
|
||||
}
|
||||
if !matchedTTL {
|
||||
return nil, trace.AccessDenied("this user cannot request kubernetes access for %v", ttl)
|
||||
return nil, nil, trace.AccessDenied("this user cannot request kubernetes access for %v", ttl)
|
||||
}
|
||||
if len(groups) == 0 {
|
||||
return nil, trace.AccessDenied("this user cannot request kubernetes access, has no assigned groups")
|
||||
if len(groups) == 0 && len(users) == 0 {
|
||||
return nil, nil, trace.AccessDenied("this user cannot request kubernetes access, has no assigned groups or users")
|
||||
}
|
||||
out := make([]string, 0, len(groups))
|
||||
for group := range groups {
|
||||
out = append(out, group)
|
||||
}
|
||||
return out, nil
|
||||
return utils.StringsSliceFromSet(groups), utils.StringsSliceFromSet(users), nil
|
||||
}
|
||||
|
||||
// CheckLoginDuration checks if role set can login up to given duration and
|
||||
|
||||
@@ -1128,6 +1128,8 @@ func (s *RoleSuite) TestApplyTraits(c *C) {
|
||||
outLabels Labels
|
||||
inKubeGroups []string
|
||||
outKubeGroups []string
|
||||
inKubeUsers []string
|
||||
outKubeUsers []string
|
||||
}
|
||||
var tests = []struct {
|
||||
comment string
|
||||
@@ -1146,6 +1148,16 @@ func (s *RoleSuite) TestApplyTraits(c *C) {
|
||||
outLogins: []string{"bar", "root"},
|
||||
},
|
||||
},
|
||||
{
|
||||
comment: "logins substitute in allow rule with function",
|
||||
inTraits: map[string][]string{
|
||||
"foo": []string{"Bar <bar@example.com>"},
|
||||
},
|
||||
allow: rule{
|
||||
inLogins: []string{`{{email.local(external.foo)}}`, "root"},
|
||||
outLogins: []string{"bar", "root"},
|
||||
},
|
||||
},
|
||||
{
|
||||
comment: "logins substitute in deny rule",
|
||||
inTraits: map[string][]string{
|
||||
@@ -1176,6 +1188,26 @@ func (s *RoleSuite) TestApplyTraits(c *C) {
|
||||
outKubeGroups: []string{"bar", "root"},
|
||||
},
|
||||
},
|
||||
{
|
||||
comment: "kube user interpolation in allow rule",
|
||||
inTraits: map[string][]string{
|
||||
"foo": []string{"bar"},
|
||||
},
|
||||
allow: rule{
|
||||
inKubeUsers: []string{`IAM#{{external.foo}};`},
|
||||
outKubeUsers: []string{"IAM#bar;"},
|
||||
},
|
||||
},
|
||||
{
|
||||
comment: "kube users interpolation in deny rule",
|
||||
inTraits: map[string][]string{
|
||||
"foo": []string{"bar"},
|
||||
},
|
||||
deny: rule{
|
||||
inKubeUsers: []string{`IAM#{{external.foo}};`},
|
||||
outKubeUsers: []string{"IAM#bar;"},
|
||||
},
|
||||
},
|
||||
{
|
||||
comment: "no variable in logins",
|
||||
inTraits: map[string][]string{
|
||||
@@ -1186,15 +1218,40 @@ func (s *RoleSuite) TestApplyTraits(c *C) {
|
||||
outLogins: []string{"root"},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
comment: "invalid variable in logins gets passed along",
|
||||
comment: "invalid variable in logins does not get passed along",
|
||||
inTraits: map[string][]string{
|
||||
"foo": []string{"bar"},
|
||||
},
|
||||
allow: rule{
|
||||
inLogins: []string{`external.foo}}`},
|
||||
outLogins: []string{`external.foo}}`},
|
||||
inLogins: []string{`external.foo}}`},
|
||||
},
|
||||
},
|
||||
{
|
||||
comment: "invalid function call in logins does not get passed along",
|
||||
inTraits: map[string][]string{
|
||||
"foo": []string{"bar"},
|
||||
},
|
||||
allow: rule{
|
||||
inLogins: []string{`{{email.local(external.foo, 1)}}`},
|
||||
},
|
||||
},
|
||||
{
|
||||
comment: "invalid function call in logins does not get passed along",
|
||||
inTraits: map[string][]string{
|
||||
"foo": []string{"bar"},
|
||||
},
|
||||
allow: rule{
|
||||
inLogins: []string{`{{email.local()}}`},
|
||||
},
|
||||
},
|
||||
{
|
||||
comment: "invalid function call in logins does not get passed along",
|
||||
inTraits: map[string][]string{
|
||||
"foo": []string{"bar"},
|
||||
},
|
||||
allow: rule{
|
||||
inLogins: []string{`{{email.local(email.local)}}`, `{{email.local(email.local())}}`},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1310,11 +1367,13 @@ func (s *RoleSuite) TestApplyTraits(c *C) {
|
||||
Logins: tt.allow.inLogins,
|
||||
NodeLabels: tt.allow.inLabels,
|
||||
KubeGroups: tt.allow.inKubeGroups,
|
||||
KubeUsers: tt.allow.inKubeUsers,
|
||||
},
|
||||
Deny: RoleConditions{
|
||||
Logins: tt.deny.inLogins,
|
||||
NodeLabels: tt.deny.inLabels,
|
||||
KubeGroups: tt.deny.inKubeGroups,
|
||||
KubeUsers: tt.deny.inKubeUsers,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1323,10 +1382,12 @@ func (s *RoleSuite) TestApplyTraits(c *C) {
|
||||
c.Assert(outRole.GetLogins(Allow), DeepEquals, tt.allow.outLogins, comment)
|
||||
c.Assert(outRole.GetNodeLabels(Allow), DeepEquals, tt.allow.outLabels, comment)
|
||||
c.Assert(outRole.GetKubeGroups(Allow), DeepEquals, tt.allow.outKubeGroups, comment)
|
||||
c.Assert(outRole.GetKubeUsers(Allow), DeepEquals, tt.allow.outKubeUsers, comment)
|
||||
|
||||
c.Assert(outRole.GetLogins(Deny), DeepEquals, tt.deny.outLogins, comment)
|
||||
c.Assert(outRole.GetNodeLabels(Deny), DeepEquals, tt.deny.outLabels, comment)
|
||||
c.Assert(outRole.GetKubeGroups(Deny), DeepEquals, tt.deny.outKubeGroups, comment)
|
||||
c.Assert(outRole.GetKubeUsers(Deny), DeepEquals, tt.deny.outKubeUsers, comment)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-48
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2015-2019 Gravitational, Inc.
|
||||
Copyright 2015-2020 Gravitational, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -17,7 +17,6 @@ limitations under the License.
|
||||
package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/base64"
|
||||
@@ -26,7 +25,6 @@ import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/gravitational/teleport"
|
||||
@@ -192,7 +190,7 @@ func (*TeleportSAMLConnectorMarshaler) UnmarshalSAMLConnector(bytes []byte, opts
|
||||
return nil, trace.BadParameter("SAML connector resource version %v is not supported", h.Version)
|
||||
}
|
||||
|
||||
// MarshalUser marshals SAML connector into JSON
|
||||
// MarshalSAMLConnector marshals SAML connector into JSON
|
||||
func (*TeleportSAMLConnectorMarshaler) MarshalSAMLConnector(c SAMLConnector, opts ...MarshalOption) ([]byte, error) {
|
||||
cfg, err := collectOptions(opts)
|
||||
if err != nil {
|
||||
@@ -507,50 +505,6 @@ func (o *SAMLConnectorV2) MapAttributes(assertionInfo saml2.AssertionInfo) []str
|
||||
return utils.Deduplicate(roles)
|
||||
}
|
||||
|
||||
// executeSAMLStringTemplate takes a raw template string and a map of
|
||||
// assertions to execute a template and generate output. Because the data
|
||||
// structure used to execute the template is a map, the format of the raw
|
||||
// string is expected to be {{index . "key"}}. See
|
||||
// https://golang.org/pkg/text/template/ for more details.
|
||||
func executeSAMLStringTemplate(raw string, assertion map[string]string) (string, error) {
|
||||
tmpl, err := template.New("dynamic-roles").Parse(raw)
|
||||
if err != nil {
|
||||
return "", trace.Wrap(err)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
err = tmpl.Execute(&buf, assertion)
|
||||
if err != nil {
|
||||
return "", trace.Wrap(err)
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// executeSAMLStringTemplate takes raw template strings and a map of
|
||||
// assertions to execute templates and generate a slice of output. Because the
|
||||
// data structure used to execute the template is a map, the format of each raw
|
||||
// string is expected to be {{index . "key"}}. See
|
||||
// https://golang.org/pkg/text/template/ for more details.
|
||||
func executeSAMLSliceTemplate(raw []string, assertion map[string]string) ([]string, error) {
|
||||
var sl []string
|
||||
|
||||
for _, v := range raw {
|
||||
tmpl, err := template.New("dynamic-roles").Parse(v)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
err = tmpl.Execute(&buf, assertion)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
sl = append(sl, buf.String())
|
||||
}
|
||||
|
||||
return sl, nil
|
||||
}
|
||||
|
||||
// GetServiceProvider initialises service provider spec from settings
|
||||
func (o *SAMLConnectorV2) GetServiceProvider(clock clockwork.Clock) (*saml2.SAMLServiceProvider, error) {
|
||||
if o.Metadata.Name == "" {
|
||||
|
||||
+361
-307
File diff suppressed because it is too large
Load Diff
@@ -566,6 +566,9 @@ message RoleConditions {
|
||||
repeated string KubeGroups = 5 [(gogoproto.jsontag) = "kubernetes_groups,omitempty"];
|
||||
|
||||
AccessRequestConditions Request = 6 [(gogoproto.jsontag) = "request,omitempty"];
|
||||
|
||||
// KubeUsers is an optional kubernetes users to impersonate
|
||||
repeated string KubeUsers = 7 [(gogoproto.jsontag) = "kubernetes_users,omitempty"];
|
||||
}
|
||||
|
||||
// AccessRequestConditions is a matcher for allow/deny restrictions on access-requests.
|
||||
|
||||
@@ -484,7 +484,7 @@ func (u *UserV1) Check() error {
|
||||
return trace.BadParameter("user name cannot be empty")
|
||||
}
|
||||
for _, login := range u.AllowedLogins {
|
||||
_, _, err := parse.IsRoleVariable(login)
|
||||
_, err := parse.RoleVariable(login)
|
||||
if err == nil {
|
||||
return trace.BadParameter("role variables not allowed in allowed logins")
|
||||
}
|
||||
|
||||
+74
-11
@@ -21,6 +21,7 @@ import (
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"net"
|
||||
@@ -77,6 +78,8 @@ type Identity struct {
|
||||
Principals []string
|
||||
// KubernetesGroups is a list of Kubernetes groups allowed
|
||||
KubernetesGroups []string
|
||||
// KubernetesUsers is a list of Kubernetes users allowed
|
||||
KubernetesUsers []string
|
||||
// Expires specifies whenever the session will expire
|
||||
Expires time.Time
|
||||
// RouteToCluster specifies the target cluster
|
||||
@@ -97,6 +100,21 @@ func (i *Identity) CheckAndSetDefaults() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Custom ranges are taken from this article
|
||||
//
|
||||
// https://serverfault.com/questions/551477/is-there-reserved-oid-space-for-internal-enterprise-cas
|
||||
//
|
||||
// http://oid-info.com/get/1.3.9999
|
||||
//
|
||||
|
||||
// KubeUsersASN1ExtensionOID is an extension ID used when encoding/decoding
|
||||
// license payload into certificates
|
||||
var KubeUsersASN1ExtensionOID = asn1.ObjectIdentifier{1, 3, 9999, 1, 1}
|
||||
|
||||
// KubeGroupsASN1ExtensionOID is an extension ID used when encoding/decoding
|
||||
// license payload into certificates
|
||||
var KubeGroupsASN1ExtensionOID = asn1.ObjectIdentifier{1, 3, 9999, 1, 2}
|
||||
|
||||
// Subject converts identity to X.509 subject name
|
||||
func (id *Identity) Subject() (pkix.Name, error) {
|
||||
rawTraits, err := wrappers.MarshalTraits(&id.Traits)
|
||||
@@ -110,37 +128,82 @@ func (id *Identity) Subject() (pkix.Name, error) {
|
||||
subject.Organization = append([]string{}, id.Groups...)
|
||||
subject.OrganizationalUnit = append([]string{}, id.Usage...)
|
||||
subject.Locality = append([]string{}, id.Principals...)
|
||||
|
||||
// DELETE IN (5.0.0)
|
||||
// Groups are marshaled to both ASN1 extension
|
||||
// and old Province section, for backwards-compatibility,
|
||||
// however begin migration to ASN1 extensions in the future
|
||||
// for this and other properties
|
||||
subject.Province = append([]string{}, id.KubernetesGroups...)
|
||||
subject.StreetAddress = []string{id.RouteToCluster}
|
||||
subject.PostalCode = []string{string(rawTraits)}
|
||||
|
||||
for i := range id.KubernetesUsers {
|
||||
kubeUser := id.KubernetesUsers[i]
|
||||
subject.ExtraNames = append(subject.ExtraNames,
|
||||
pkix.AttributeTypeAndValue{
|
||||
Type: KubeUsersASN1ExtensionOID,
|
||||
Value: kubeUser,
|
||||
})
|
||||
}
|
||||
|
||||
for i := range id.KubernetesGroups {
|
||||
kubeGroup := id.KubernetesGroups[i]
|
||||
subject.ExtraNames = append(subject.ExtraNames,
|
||||
pkix.AttributeTypeAndValue{
|
||||
Type: KubeGroupsASN1ExtensionOID,
|
||||
Value: kubeGroup,
|
||||
})
|
||||
}
|
||||
|
||||
return subject, nil
|
||||
}
|
||||
|
||||
// FromSubject returns identity from subject name
|
||||
func FromSubject(subject pkix.Name, expires time.Time) (*Identity, error) {
|
||||
i := &Identity{
|
||||
Username: subject.CommonName,
|
||||
Groups: subject.Organization,
|
||||
Usage: subject.OrganizationalUnit,
|
||||
Principals: subject.Locality,
|
||||
KubernetesGroups: subject.Province,
|
||||
Expires: expires,
|
||||
id := &Identity{
|
||||
Username: subject.CommonName,
|
||||
Groups: subject.Organization,
|
||||
Usage: subject.OrganizationalUnit,
|
||||
Principals: subject.Locality,
|
||||
Expires: expires,
|
||||
}
|
||||
if len(subject.StreetAddress) > 0 {
|
||||
i.RouteToCluster = subject.StreetAddress[0]
|
||||
id.RouteToCluster = subject.StreetAddress[0]
|
||||
}
|
||||
if len(subject.PostalCode) > 0 {
|
||||
err := wrappers.UnmarshalTraits([]byte(subject.PostalCode[0]), &i.Traits)
|
||||
err := wrappers.UnmarshalTraits([]byte(subject.PostalCode[0]), &id.Traits)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := i.CheckAndSetDefaults(); err != nil {
|
||||
for _, attr := range subject.Names {
|
||||
switch {
|
||||
case attr.Type.Equal(KubeUsersASN1ExtensionOID):
|
||||
val, ok := attr.Value.(string)
|
||||
if ok {
|
||||
id.KubernetesUsers = append(id.KubernetesUsers, val)
|
||||
}
|
||||
case attr.Type.Equal(KubeGroupsASN1ExtensionOID):
|
||||
val, ok := attr.Value.(string)
|
||||
if ok {
|
||||
id.KubernetesGroups = append(id.KubernetesGroups, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE IN(5.0.0): This logic is using Province field
|
||||
// from subject in case if Kubernetes groups were not populated
|
||||
// from ASN1 extension, after 5.0 Province field will be ignored
|
||||
if len(id.KubernetesGroups) == 0 {
|
||||
id.KubernetesGroups = subject.Province
|
||||
}
|
||||
|
||||
if err := id.CheckAndSetDefaults(); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
return i, nil
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// CertificateRequest is a X.509 signing certificate request
|
||||
|
||||
@@ -71,3 +71,42 @@ func (s *TLSCASuite) TestPrincipals(c *check.C) {
|
||||
}
|
||||
c.Assert(certIPs, check.DeepEquals, ips)
|
||||
}
|
||||
|
||||
// TestKubeExtensions test ASN1 subject kubernetes extensions
|
||||
func (s *TLSCASuite) TestKubeExtensions(c *check.C) {
|
||||
ca, err := New([]byte(fixtures.SigningCertPEM), []byte(fixtures.SigningKeyPEM))
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, teleport.RSAKeySize)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
expires := s.clock.Now().Add(time.Hour)
|
||||
identity := Identity{
|
||||
Username: "alice@example.com",
|
||||
Groups: []string{"admin"},
|
||||
// Generate a certificate restricted for
|
||||
// use against a kubernetes endpoint, and not the API server endpoint
|
||||
// otherwise proxies can generate certs for any user.
|
||||
Usage: []string{teleport.UsageKubeOnly},
|
||||
KubernetesGroups: []string{"system:masters", "admin"},
|
||||
KubernetesUsers: []string{"IAM#alice@example.com"},
|
||||
Expires: expires,
|
||||
}
|
||||
|
||||
subj, err := identity.Subject()
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
certBytes, err := ca.GenerateCertificate(CertificateRequest{
|
||||
Clock: s.clock,
|
||||
PublicKey: privateKey.Public(),
|
||||
Subject: subj,
|
||||
NotAfter: expires,
|
||||
})
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
cert, err := ParseCertificatePEM(certBytes)
|
||||
c.Assert(err, check.IsNil)
|
||||
out, err := FromSubject(cert.Subject, cert.NotAfter)
|
||||
c.Assert(err, check.IsNil)
|
||||
fixtures.DeepCompare(c, out, &identity)
|
||||
}
|
||||
|
||||
+164
-27
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2017 Gravitational, Inc.
|
||||
Copyright 2017-2020 Gravitational, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -19,87 +19,224 @@ package parse
|
||||
import (
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"net/mail"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
)
|
||||
|
||||
// IsRoleVariable checks if the passed in string matches the variable pattern
|
||||
// Expression is an expression template
|
||||
// that can interpolate to some variables
|
||||
type Expression struct {
|
||||
// namespace is expression namespace,
|
||||
// e.g. internal.traits has a variable traits
|
||||
// in internal namespace
|
||||
namespace string
|
||||
// variable is a variable name, e.g. trait name,
|
||||
// e.g. internal.traits has variable name traits
|
||||
variable string
|
||||
// prefix is a prefix of the string
|
||||
prefix string
|
||||
// suffix is a suffix
|
||||
suffix string
|
||||
// transform is an optional transform function to call,
|
||||
// currently email.local is the only supported function
|
||||
transform func(in string) (string, error)
|
||||
}
|
||||
|
||||
// EmailLocal returns local part of the email
|
||||
func EmailLocal(in string) (string, error) {
|
||||
if in == "" {
|
||||
return "", trace.BadParameter("address is empty")
|
||||
}
|
||||
addr, err := mail.ParseAddress(in)
|
||||
if err != nil {
|
||||
return "", trace.BadParameter("failed to parse address %q: %q", in, err)
|
||||
}
|
||||
parts := strings.SplitN(addr.Address, "@", 2)
|
||||
if len(parts) != 2 {
|
||||
return "", trace.BadParameter("could not find local part in %q", addr.Address)
|
||||
}
|
||||
return parts[0], nil
|
||||
}
|
||||
|
||||
// Namespace returns a variable namespace, e.g. external or internal
|
||||
func (p *Expression) Namespace() string {
|
||||
return p.namespace
|
||||
}
|
||||
|
||||
// Name returns variable name
|
||||
func (p *Expression) Name() string {
|
||||
return p.variable
|
||||
}
|
||||
|
||||
// Interpolate interpolates the variable adding prefix and suffix if present,
|
||||
// returns trace.NotFound in case if the trait is not found, nil in case of
|
||||
// success and BadParameter error otherwise
|
||||
func (p *Expression) Interpolate(traits map[string][]string) ([]string, error) {
|
||||
values, ok := traits[p.variable]
|
||||
if !ok {
|
||||
return nil, trace.NotFound("variable is not found")
|
||||
}
|
||||
out := make([]string, len(values))
|
||||
for i := range values {
|
||||
val := values[i]
|
||||
var err error
|
||||
if p.transform != nil {
|
||||
val, err = p.transform(val)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
}
|
||||
out[i] = p.prefix + val + p.suffix
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
var reVariable = regexp.MustCompile(
|
||||
// prefix is anyting that is not { or }
|
||||
`^(?P<prefix>[^}{]*)` +
|
||||
// variable is antything in brackets {{}} that is not { or }
|
||||
`{{(?P<expression>\s*[^}{]*\s*)}}` +
|
||||
// prefix is anyting that is not { or }
|
||||
`(?P<suffix>[^}{]*)$`,
|
||||
)
|
||||
|
||||
// RoleVariable checks if the passed in string matches the variable pattern
|
||||
// {{external.foo}} or {{internal.bar}}. If it does, it returns the variable
|
||||
// prefix and the variable name. In the previous example this would be
|
||||
// "external" or "internal" for the variable prefix and "foo" or "bar" for the
|
||||
// variable name. If no variable pattern is found, trace.NotFound is returned.
|
||||
func IsRoleVariable(variable string) (string, string, error) {
|
||||
// time whitespace around string if it exists
|
||||
variable = strings.TrimSpace(variable)
|
||||
|
||||
// strip {{ and }} from the start and end of the variable
|
||||
if !strings.HasPrefix(variable, "{{") || !strings.HasSuffix(variable, "}}") {
|
||||
return "", "", trace.NotFound("no variable found: %v", variable)
|
||||
func RoleVariable(variable string) (*Expression, error) {
|
||||
match := reVariable.FindStringSubmatch(variable)
|
||||
if len(match) == 0 {
|
||||
if strings.Index(variable, "{{") != -1 || strings.Index(variable, "}}") != -1 {
|
||||
return nil, trace.BadParameter(
|
||||
"%q is using template brackets '{{' or '}}', however expression does not parse, make sure the format is {{variable}}",
|
||||
variable)
|
||||
}
|
||||
return nil, trace.NotFound("no variable found in %q", variable)
|
||||
}
|
||||
variable = variable[2 : len(variable)-2]
|
||||
|
||||
prefix, variable, suffix := match[1], match[2], match[3]
|
||||
|
||||
// parse and get the ast of the expression
|
||||
expr, err := parser.ParseExpr(variable)
|
||||
if err != nil {
|
||||
return "", "", trace.NotFound("no variable found: %v", variable)
|
||||
return nil, trace.NotFound("no variable found in %q: %v", variable, err)
|
||||
}
|
||||
|
||||
// walk the ast tree and gather the variable parts
|
||||
variableParts, err := walk(expr)
|
||||
result, err := walk(expr)
|
||||
if err != nil {
|
||||
return "", "", trace.NotFound("no variable found: %v", variable)
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// the variable must have two parts the prefix and the variable name itself
|
||||
if len(variableParts) != 2 {
|
||||
return "", "", trace.NotFound("no variable found: %v", variable)
|
||||
if len(result.parts) != 2 {
|
||||
return nil, trace.NotFound("no variable found: %v", variable)
|
||||
}
|
||||
|
||||
return variableParts[0], variableParts[1], nil
|
||||
return &Expression{
|
||||
prefix: strings.TrimLeftFunc(prefix, unicode.IsSpace),
|
||||
namespace: result.parts[0],
|
||||
variable: result.parts[1],
|
||||
suffix: strings.TrimRightFunc(suffix, unicode.IsSpace),
|
||||
transform: result.transform,
|
||||
}, nil
|
||||
}
|
||||
|
||||
const (
|
||||
// EmailNamespace is a function namespace for email functions
|
||||
EmailNamespace = "email"
|
||||
// EmailLocalFnName is a name for email.local function
|
||||
EmailLocalFnName = "local"
|
||||
)
|
||||
|
||||
// TransformFn is an optional transform function
|
||||
// that can take in string and replace it with another value
|
||||
type TransformFn func(in string) (string, error)
|
||||
|
||||
type walkResult struct {
|
||||
parts []string
|
||||
transform TransformFn
|
||||
}
|
||||
|
||||
// walk will walk the ast tree and gather all the variable parts into a slice and return it.
|
||||
func walk(node ast.Node) ([]string, error) {
|
||||
var l []string
|
||||
func walk(node ast.Node) (*walkResult, error) {
|
||||
var result walkResult
|
||||
|
||||
switch n := node.(type) {
|
||||
case *ast.CallExpr:
|
||||
switch call := n.Fun.(type) {
|
||||
case *ast.Ident:
|
||||
return nil, trace.BadParameter("function %v is not supported", call.Name)
|
||||
case *ast.SelectorExpr:
|
||||
// Selector expression looks like email.local(parameter)
|
||||
namespace, ok := call.X.(*ast.Ident)
|
||||
if !ok {
|
||||
return nil, trace.BadParameter("expected namespace, e.g. email.local, got %v", call.X)
|
||||
}
|
||||
// This is the part before the dot
|
||||
if namespace.Name != EmailNamespace {
|
||||
return nil, trace.BadParameter("unsupported namespace, e.g. email.local, got %v", call.X)
|
||||
}
|
||||
// This is a function name
|
||||
if call.Sel.Name != EmailLocalFnName {
|
||||
return nil, trace.BadParameter("unsupported function %v, supported functions are: email.local", call.Sel.Name)
|
||||
}
|
||||
// Because only one function is supported for now,
|
||||
// this makes sure that the function call has exactly one argument
|
||||
if len(n.Args) != 1 {
|
||||
return nil, trace.BadParameter("expected 1 argument for email.local got %v", len(n.Args))
|
||||
}
|
||||
result.transform = EmailLocal
|
||||
ret, err := walk(n.Args[0])
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
result.parts = ret.parts
|
||||
return &result, nil
|
||||
default:
|
||||
return nil, trace.BadParameter("unsupported function %T", n.Fun)
|
||||
}
|
||||
case *ast.IndexExpr:
|
||||
ret, err := walk(n.X)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l = append(l, ret...)
|
||||
|
||||
result.parts = append(result.parts, ret.parts...)
|
||||
ret, err = walk(n.Index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l = append(l, ret...)
|
||||
result.parts = append(result.parts, ret.parts...)
|
||||
return &result, nil
|
||||
case *ast.SelectorExpr:
|
||||
ret, err := walk(n.X)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l = append(l, ret...)
|
||||
result.parts = append(result.parts, ret.parts...)
|
||||
|
||||
ret, err = walk(n.Sel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l = append(l, ret...)
|
||||
result.parts = append(result.parts, ret.parts...)
|
||||
return &result, nil
|
||||
case *ast.Ident:
|
||||
return []string{n.Name}, nil
|
||||
return &walkResult{parts: []string{n.Name}}, nil
|
||||
case *ast.BasicLit:
|
||||
value, err := strconv.Unquote(n.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []string{value}, nil
|
||||
return &walkResult{parts: []string{value}}, nil
|
||||
default:
|
||||
return nil, trace.BadParameter("unknown node type: %T", n)
|
||||
}
|
||||
|
||||
return l, nil
|
||||
}
|
||||
|
||||
+126
-62
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2017 Gravitational, Inc.
|
||||
Copyright 2017-2020 Gravitational, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -40,94 +40,158 @@ func (s *ParseSuite) TearDownSuite(c *check.C) {}
|
||||
func (s *ParseSuite) SetUpTest(c *check.C) {}
|
||||
func (s *ParseSuite) TearDownTest(c *check.C) {}
|
||||
|
||||
func (s *ParseSuite) TestIsRoleVariable(c *check.C) {
|
||||
// TestRoleVariable tests variable parsing
|
||||
func (s *ParseSuite) TestRoleVariable(c *check.C) {
|
||||
var tests = []struct {
|
||||
inVariable string
|
||||
outIsNotFound bool
|
||||
outVariablePrefix string
|
||||
outVariableName string
|
||||
title string
|
||||
in string
|
||||
err error
|
||||
out Expression
|
||||
}{
|
||||
// 0 - no curly bracket prefix
|
||||
{
|
||||
"external.foo}}",
|
||||
true,
|
||||
"",
|
||||
"",
|
||||
title: "no curly bracket prefix",
|
||||
in: "external.foo}}",
|
||||
err: trace.BadParameter(""),
|
||||
},
|
||||
// 1 - invalid syntax
|
||||
{
|
||||
`{{external.foo("bar")`,
|
||||
true,
|
||||
"",
|
||||
"",
|
||||
title: "invalid syntax",
|
||||
in: `{{external.foo("bar")`,
|
||||
err: trace.BadParameter(""),
|
||||
},
|
||||
// 2 - invalid sytnax
|
||||
{
|
||||
"{{internal.}}",
|
||||
true,
|
||||
"",
|
||||
"",
|
||||
title: "invalid variable syntax",
|
||||
in: "{{internal.}}",
|
||||
err: trace.BadParameter(""),
|
||||
},
|
||||
// 3 - invalid syntax
|
||||
{
|
||||
"{{external..foo}}",
|
||||
true,
|
||||
"",
|
||||
"",
|
||||
title: "invalid dot syntax",
|
||||
in: "{{external..foo}}",
|
||||
err: trace.BadParameter(""),
|
||||
},
|
||||
// 4 - invalid syntax
|
||||
{
|
||||
"{{}}",
|
||||
true,
|
||||
"",
|
||||
"",
|
||||
title: "empty variable",
|
||||
in: "{{}}",
|
||||
err: trace.BadParameter(""),
|
||||
},
|
||||
// 5 - invalid syntax
|
||||
{
|
||||
"{{internal.foo",
|
||||
true,
|
||||
"",
|
||||
"",
|
||||
title: "no curly bracket suffix",
|
||||
in: "{{internal.foo",
|
||||
err: trace.BadParameter(""),
|
||||
},
|
||||
// 6 - invalid syntax
|
||||
{
|
||||
"{{internal.foo.bar}}",
|
||||
true,
|
||||
"",
|
||||
"",
|
||||
title: "too many levels of nesting in the variable",
|
||||
in: "{{internal.foo.bar}}",
|
||||
err: trace.BadParameter(""),
|
||||
},
|
||||
// 7 - valid brackets
|
||||
{
|
||||
`{{internal["foo"]}}`,
|
||||
false,
|
||||
"internal",
|
||||
"foo",
|
||||
title: "valid with brackets",
|
||||
in: `{{internal["foo"]}}`,
|
||||
out: Expression{namespace: "internal", variable: "foo"},
|
||||
},
|
||||
// 8 - valid
|
||||
{
|
||||
"{{external.foo}}",
|
||||
false,
|
||||
"external",
|
||||
"foo",
|
||||
title: "external with no brackets",
|
||||
in: "{{external.foo}}",
|
||||
out: Expression{namespace: "external", variable: "foo"},
|
||||
},
|
||||
// 9 - valid
|
||||
{
|
||||
"{{internal.bar}}",
|
||||
false,
|
||||
"internal",
|
||||
"bar",
|
||||
title: "internal with no brackets",
|
||||
in: "{{internal.bar}}",
|
||||
out: Expression{namespace: "internal", variable: "bar"},
|
||||
},
|
||||
{
|
||||
title: "internal with spaces removed",
|
||||
in: " {{ internal.bar }} ",
|
||||
out: Expression{namespace: "internal", variable: "bar"},
|
||||
},
|
||||
{
|
||||
title: "variable with prefix and suffix",
|
||||
in: " hello, {{ internal.bar }} there! ",
|
||||
out: Expression{prefix: "hello, ", namespace: "internal", variable: "bar", suffix: " there!"},
|
||||
},
|
||||
{
|
||||
title: "variable with local function",
|
||||
in: "{{email.local(internal.bar)}}",
|
||||
out: Expression{namespace: "internal", variable: "bar", transform: EmailLocal},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
comment := check.Commentf("Test %v", i)
|
||||
comment := check.Commentf("Test(%v) %q", i, tt.title)
|
||||
|
||||
variablePrefix, variableName, err := IsRoleVariable(tt.inVariable)
|
||||
if tt.outIsNotFound {
|
||||
c.Assert(trace.IsNotFound(err), check.Equals, true, comment)
|
||||
variable, err := RoleVariable(tt.in)
|
||||
if tt.err != nil {
|
||||
c.Assert(err, check.FitsTypeOf, tt.err, comment)
|
||||
continue
|
||||
}
|
||||
c.Assert(variablePrefix, check.Equals, tt.outVariablePrefix, comment)
|
||||
c.Assert(variableName, check.Equals, tt.outVariableName, comment)
|
||||
c.Assert(err, check.IsNil, comment)
|
||||
// functionns are not directly comparable, compare fields
|
||||
// directly, except functions as a workaround
|
||||
c.Assert(variable.prefix, check.Equals, tt.out.prefix, comment)
|
||||
c.Assert(variable.variable, check.Equals, tt.out.variable, comment)
|
||||
c.Assert(variable.suffix, check.Equals, tt.out.suffix, comment)
|
||||
// functions are not comparable
|
||||
if tt.out.transform == nil {
|
||||
c.Assert(variable.transform, check.IsNil, comment)
|
||||
} else {
|
||||
c.Assert(variable.transform, check.NotNil, comment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestInterpolate tests variable interpolation
|
||||
func (s *ParseSuite) TestInterpolate(c *check.C) {
|
||||
type result struct {
|
||||
values []string
|
||||
err error
|
||||
}
|
||||
var tests = []struct {
|
||||
title string
|
||||
in Expression
|
||||
traits map[string][]string
|
||||
res result
|
||||
}{
|
||||
{
|
||||
title: "mapped traits",
|
||||
in: Expression{variable: "foo"},
|
||||
traits: map[string][]string{"foo": []string{"a", "b"}, "bar": []string{"c"}},
|
||||
res: result{values: []string{"a", "b"}},
|
||||
},
|
||||
{
|
||||
title: "mapped traits with email.local",
|
||||
in: Expression{variable: "foo", transform: EmailLocal},
|
||||
traits: map[string][]string{"foo": []string{"Alice <alice@example.com>", "bob@example.com"}, "bar": []string{"c"}},
|
||||
res: result{values: []string{"alice", "bob"}},
|
||||
},
|
||||
{
|
||||
title: "missed traits",
|
||||
in: Expression{variable: "baz"},
|
||||
traits: map[string][]string{"foo": []string{"a", "b"}, "bar": []string{"c"}},
|
||||
res: result{err: trace.NotFound("not found"), values: []string{}},
|
||||
},
|
||||
{
|
||||
title: "traits with prefix and suffix",
|
||||
in: Expression{prefix: "IAM#", variable: "foo", suffix: ";"},
|
||||
traits: map[string][]string{"foo": []string{"a", "b"}, "bar": []string{"c"}},
|
||||
res: result{values: []string{"IAM#a;", "IAM#b;"}},
|
||||
},
|
||||
{
|
||||
title: "error in mapping traits",
|
||||
in: Expression{variable: "foo", transform: EmailLocal},
|
||||
traits: map[string][]string{"foo": []string{"Alice <alice"}},
|
||||
res: result{err: trace.BadParameter("")},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
comment := check.Commentf("Test(%v) %q", i, tt.title)
|
||||
|
||||
values, err := tt.in.Interpolate(tt.traits)
|
||||
if tt.res.err != nil {
|
||||
c.Assert(err, check.FitsTypeOf, tt.res.err, comment)
|
||||
c.Assert(values, check.HasLen, 0)
|
||||
continue
|
||||
}
|
||||
c.Assert(err, check.IsNil, comment)
|
||||
c.Assert(values, check.DeepEquals, tt.res.values, comment)
|
||||
}
|
||||
}
|
||||
|
||||
+16
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2015-2019 Gravitational, Inc.
|
||||
Copyright 2015-2020 Gravitational, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -178,11 +179,24 @@ func ParseAdvertiseAddr(advertiseIP string) (string, string, error) {
|
||||
return host, port, nil
|
||||
}
|
||||
|
||||
// StringsSliceFromSet returns a sorted strings slice from set
|
||||
func StringsSliceFromSet(in map[string]struct{}) []string {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, 0, len(in))
|
||||
for key := range in {
|
||||
out = append(out, key)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// StringsSet creates set of string (map[string]struct{})
|
||||
// from a list of strings
|
||||
func StringsSet(in []string) map[string]struct{} {
|
||||
if in == nil {
|
||||
return nil
|
||||
return map[string]struct{}{}
|
||||
}
|
||||
out := make(map[string]struct{})
|
||||
for _, v := range in {
|
||||
|
||||
@@ -500,3 +500,10 @@ func (s *UtilsSuite) TestReadToken(c *check.C) {
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(tok, check.Equals, "shmoken")
|
||||
}
|
||||
|
||||
// TestStringsSet makes sure that nil slice returns empty set (less error prone)
|
||||
func (s *UtilsSuite) TestStringsSet(c *check.C) {
|
||||
out := StringsSet(nil)
|
||||
c.Assert(out, check.HasLen, 0)
|
||||
c.Assert(out, check.NotNil)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user