Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for setting an Alias' name from the k8s namespace and serviceaccount #110

Merged
merged 19 commits into from Sep 13, 2021
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
41 changes: 39 additions & 2 deletions backend.go
Expand Up @@ -12,8 +12,24 @@ import (
)

const (
configPath string = "config"
rolePrefix string = "role/"
configPath = "config"
rolePrefix = "role/"

// aliasNameSourceUnset provides backwards compatibility with preexisting roles.
aliasNameSourceUnset = ""
aliasNameSourceSAToken = "sa_token"
aliasNameSourceSAPath = "sa_path"
aliasNameSourceDefault = aliasNameSourceSAToken
)

var (
aliasNameSources = []string{aliasNameSourceSAToken, aliasNameSourceSAPath}
// map alias name source to its description
aliasNameSourceMap = map[string]string{
aliasNameSourceSAToken: "format: <token.uid>",
aliasNameSourceSAPath: "format: <namespace>/<serviceaccount>",
}
errInvalidAliasNameSource = fmt.Errorf(`invalid alias_name_source, must be one of: %s`, strings.Join(aliasNameSources, ", "))
)

// kubeAuthBackend implements logical.Backend
Expand Down Expand Up @@ -132,6 +148,27 @@ func (b *kubeAuthBackend) role(ctx context.Context, s logical.Storage, name stri
return role, nil
}

func validateAliasNameSource(source string) error {
for _, s := range aliasNameSources {
if s == source {
return nil
}
}
return errInvalidAliasNameSource
}

func getAliasNameSourceDesc() string {
benashz marked this conversation as resolved.
Show resolved Hide resolved
var desc = make([]string, len(aliasNameSources))
for i, s := range aliasNameSources {
d := aliasNameSourceMap[s]
if s == aliasNameSourceDefault {
d = d + " [default]"
}
desc[i] = fmt.Sprintf("%q (%s)", s, d)
}
return strings.Join(desc, ", ")
}

var backendHelp string = `
The Kubernetes Auth Backend allows authentication for Kubernetes service accounts.
`
18 changes: 17 additions & 1 deletion path_login.go
Expand Up @@ -100,6 +100,11 @@ func (b *kubeAuthBackend) pathLogin(ctx context.Context, req *logical.Request, d
return nil, err
}

aliasName, err := b.getAliasName(role, serviceAccount)
if err != nil {
return nil, err
}

// look up the JWT token in the kubernetes API
err = serviceAccount.lookup(ctx, jwtStr, b.reviewFactory(config))
if err != nil {
Expand All @@ -109,7 +114,7 @@ func (b *kubeAuthBackend) pathLogin(ctx context.Context, req *logical.Request, d

auth := &logical.Auth{
Alias: &logical.Alias{
Name: serviceAccount.uid(),
Name: aliasName,
Metadata: map[string]string{
"service_account_uid": serviceAccount.uid(),
"service_account_name": serviceAccount.name(),
Expand Down Expand Up @@ -137,6 +142,17 @@ func (b *kubeAuthBackend) pathLogin(ctx context.Context, req *logical.Request, d
}, nil
}

func (b *kubeAuthBackend) getAliasName(role *roleStorageEntry, serviceAccount *serviceAccount) (string, error) {
switch role.AliasNameSource {
case aliasNameSourceSAToken, aliasNameSourceUnset:
return serviceAccount.uid(), nil
case aliasNameSourceSAPath:
return fmt.Sprintf("%s/%s", serviceAccount.Namespace, serviceAccount.Name), nil
default:
return "", fmt.Errorf("unknown alias_name_source %q", role.AliasNameSource)
}
}

// aliasLookahead returns the alias object with the SA UID from the JWT
// Claims.
func (b *kubeAuthBackend) aliasLookahead(_ context.Context, _ *logical.Request, data *framework.FieldData) (*logical.Response, error) {
benashz marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
79 changes: 56 additions & 23 deletions path_login_test.go
Expand Up @@ -31,18 +31,28 @@ var (
testNoPEMs = []string{testECCert, testRSACert}
)

func setupBackend(t *testing.T, pems []string, saName string, saNamespace string) (logical.Backend, logical.Storage) {
b, storage := getBackend(t)
type testBackendConfig struct {
pems []string
saName string
saNamespace string
aliasNameSource string
}

func defaultTestBackendConfig() *testBackendConfig {
return &testBackendConfig{
pems: testDefaultPEMs,
saName: testName,
saNamespace: testNamespace,
aliasNameSource: aliasNameSourceDefault,
}
}

// pems := []string{testECCert, testRSACert, testMinikubePubKey}
// pems := []string{testECCert, testRSACert}
// if noPEMs {
// pems = []string{}
// }
func setupBackend(t *testing.T, config *testBackendConfig) (logical.Backend, logical.Storage) {
b, storage := getBackend(t)

// test no certificate
data := map[string]interface{}{
"pem_keys": pems,
"pem_keys": config.pems,
"kubernetes_host": "host",
"kubernetes_ca_cert": testCACert,
}
Expand All @@ -60,13 +70,14 @@ func setupBackend(t *testing.T, pems []string, saName string, saNamespace string
}

data = map[string]interface{}{
"bound_service_account_names": saName,
"bound_service_account_namespaces": saNamespace,
"bound_service_account_names": config.saName,
"bound_service_account_namespaces": config.saNamespace,
"policies": "test",
"period": "3s",
"ttl": "1s",
"num_uses": 12,
"max_ttl": "5s",
"alias_name_source": config.aliasNameSource,
}

req = &logical.Request{
Expand All @@ -86,7 +97,7 @@ func setupBackend(t *testing.T, pems []string, saName string, saNamespace string
}

func TestLogin(t *testing.T) {
b, storage := setupBackend(t, testDefaultPEMs, testName, testNamespace)
b, storage := setupBackend(t, defaultTestBackendConfig())

// Test bad inputs
data := map[string]interface{}{
Expand Down Expand Up @@ -217,7 +228,9 @@ func TestLogin(t *testing.T) {
}

// test successful login for globbed name
b, storage = setupBackend(t, testDefaultPEMs, testGlobbedName, testNamespace)
config := defaultTestBackendConfig()
config.saName = testGlobbedName
b, storage = setupBackend(t, config)

data = map[string]interface{}{
"role": "plugin-test",
Expand All @@ -240,7 +253,9 @@ func TestLogin(t *testing.T) {
}

// test successful login for globbed namespace
b, storage = setupBackend(t, testDefaultPEMs, testName, testGlobbedNamespace)
config = defaultTestBackendConfig()
config.saNamespace = testGlobbedNamespace
b, storage = setupBackend(t, config)

data = map[string]interface{}{
"role": "plugin-test",
Expand All @@ -264,7 +279,7 @@ func TestLogin(t *testing.T) {
}

func TestLogin_ContextError(t *testing.T) {
b, storage := setupBackend(t, testDefaultPEMs, testName, testNamespace)
b, storage := setupBackend(t, defaultTestBackendConfig())

data := map[string]interface{}{
"role": "plugin-test",
Expand All @@ -291,7 +306,9 @@ func TestLogin_ContextError(t *testing.T) {
}

func TestLogin_ECDSA_PEM(t *testing.T) {
b, storage := setupBackend(t, testNoPEMs, testName, testNamespace)
config := defaultTestBackendConfig()
config.pems = testNoPEMs
b, storage := setupBackend(t, config)

// test no certificate
data := map[string]interface{}{
Expand Down Expand Up @@ -335,7 +352,9 @@ func TestLogin_ECDSA_PEM(t *testing.T) {
}

func TestLogin_NoPEMs(t *testing.T) {
b, storage := setupBackend(t, testNoPEMs, testName, testNamespace)
config := defaultTestBackendConfig()
config.pems = testNoPEMs
b, storage := setupBackend(t, config)

// test bad jwt service account
data := map[string]interface{}{
Expand Down Expand Up @@ -383,7 +402,10 @@ func TestLogin_NoPEMs(t *testing.T) {
}

func TestLoginSvcAcctAndNamespaceSplats(t *testing.T) {
b, storage := setupBackend(t, testDefaultPEMs, "*", "*")
config := defaultTestBackendConfig()
config.saName = "*"
config.saNamespace = "*"
b, storage := setupBackend(t, config)

// Test bad inputs
data := map[string]interface{}{
Expand Down Expand Up @@ -514,7 +536,9 @@ func TestLoginSvcAcctAndNamespaceSplats(t *testing.T) {
}

// test successful login for globbed name
b, storage = setupBackend(t, testDefaultPEMs, testGlobbedName, testNamespace)
config = defaultTestBackendConfig()
config.saName = testGlobbedName
b, storage = setupBackend(t, config)

data = map[string]interface{}{
"role": "plugin-test",
Expand All @@ -537,7 +561,9 @@ func TestLoginSvcAcctAndNamespaceSplats(t *testing.T) {
}

// test successful login for globbed namespace
b, storage = setupBackend(t, testDefaultPEMs, testName, testGlobbedNamespace)
config = defaultTestBackendConfig()
config.saNamespace = testGlobbedNamespace
b, storage = setupBackend(t, config)

data = map[string]interface{}{
"role": "plugin-test",
Expand All @@ -561,7 +587,7 @@ func TestLoginSvcAcctAndNamespaceSplats(t *testing.T) {
}

func TestAliasLookAhead(t *testing.T) {
b, storage := setupBackend(t, testDefaultPEMs, testName, testNamespace)
b, storage := setupBackend(t, defaultTestBackendConfig())

// Test bad inputs
data := map[string]interface{}{
Expand Down Expand Up @@ -589,7 +615,9 @@ func TestAliasLookAhead(t *testing.T) {
}

func TestLoginIssValidation(t *testing.T) {
b, storage := setupBackend(t, testNoPEMs, testName, testNamespace)
config := defaultTestBackendConfig()
config.pems = testNoPEMs
b, storage := setupBackend(t, config)

// test iss validation enabled with default "kubernetes/serviceaccount" issuer
data := map[string]interface{}{
Expand Down Expand Up @@ -768,7 +796,9 @@ Pk9Yf9rIf374m5XP1U8q79dBhLSIuaojsvOT39UUcPJROSD1FqYLued0rXiooIii
-----END PUBLIC KEY-----`

func TestLoginProjectedToken(t *testing.T) {
b, storage := setupBackend(t, append(testDefaultPEMs, testMinikubePubKey), testName, testNamespace)
config := defaultTestBackendConfig()
config.pems = append(testDefaultPEMs, testMinikubePubKey)
b, storage := setupBackend(t, config)

// update backend to accept "default" bound account name
data := map[string]interface{}{
Expand Down Expand Up @@ -879,7 +909,10 @@ func TestLoginProjectedToken(t *testing.T) {
}

func TestAliasLookAheadProjectedToken(t *testing.T) {
b, storage := setupBackend(t, append(testDefaultPEMs, testMinikubePubKey), "default", testNamespace)
config := defaultTestBackendConfig()
config.pems = append(testDefaultPEMs, testMinikubePubKey)
config.saName = "default"
b, storage := setupBackend(t, config)

data := map[string]interface{}{
"jwt": jwtProjectedData,
Expand Down
21 changes: 21 additions & 0 deletions path_role.go
Expand Up @@ -49,6 +49,11 @@ are allowed.`,
Type: framework.TypeString,
Description: "Optional Audience claim to verify in the jwt.",
},
"alias_name_source": {
Type: framework.TypeString,
Description: fmt.Sprintf(`Source to use when deriving the Alias' name, valid choices: %s`, getAliasNameSourceDesc()),
Default: aliasNameSourceDefault,
},
"policies": {
Type: framework.TypeCommaStringSlice,
Description: tokenutil.DeprecationText("token_policies"),
Expand Down Expand Up @@ -173,6 +178,8 @@ func (b *kubeAuthBackend) pathRoleRead(ctx context.Context, req *logical.Request
d["num_uses"] = role.NumUses
}

d["alias_name_source"] = role.AliasNameSource

return &logical.Response{
Data: d,
}, nil
Expand Down Expand Up @@ -302,6 +309,17 @@ func (b *kubeAuthBackend) pathRoleCreateUpdate(ctx context.Context, req *logical
role.Audience = audience.(string)
}

if source, ok := data.GetOk("alias_name_source"); ok {
if err := validateAliasNameSource(source.(string)); err != nil {
return logical.ErrorResponse(err.Error()), nil
}
role.AliasNameSource = source.(string)
} else if role.AliasNameSource == aliasNameSourceUnset {
if s, ok := data.Schema["alias_name_source"]; ok {
benashz marked this conversation as resolved.
Show resolved Hide resolved
role.AliasNameSource = s.Default.(string)
}
}

// Store the entry.
entry, err := logical.StorageEntryJSON("role/"+strings.ToLower(roleName), role)
if err != nil {
Expand Down Expand Up @@ -332,6 +350,9 @@ type roleStorageEntry struct {
// Audience is an optional jwt claim to verify
Audience string `json:"audience" mapstructure:"audience" structs: "audience"`

// AliasNameSource used when deriving the Alias' name.
AliasNameSource string `json:"alias_name_source" mapstructure:"alias_name_source" structs:"alias_name_source"`

// Deprecated by TokenParams
Policies []string `json:"policies" structs:"policies" mapstructure:"policies"`
NumUses int `json:"num_uses" mapstructure:"num_uses" structs:"num_uses"`
Expand Down
3 changes: 3 additions & 0 deletions path_role_test.go
Expand Up @@ -45,6 +45,7 @@ func TestPath_Create(t *testing.T) {
"ttl": "1s",
"num_uses": 12,
"max_ttl": "5s",
"alias_name_source": aliasNameSourceDefault,
}

expected := &roleStorageEntry{
Expand All @@ -64,6 +65,7 @@ func TestPath_Create(t *testing.T) {
MaxTTL: 5 * time.Second,
NumUses: 12,
BoundCIDRs: nil,
AliasNameSource: aliasNameSourceDefault,
}

req := &logical.Request{
Expand Down Expand Up @@ -201,6 +203,7 @@ func TestPath_Read(t *testing.T) {
"token_type": logical.TokenTypeDefault.String(),
"token_explicit_max_ttl": int64(0),
"token_no_default_policy": false,
"alias_name_source": aliasNameSourceDefault,
}

req := &logical.Request{
Expand Down