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 10 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
44 changes: 42 additions & 2 deletions backend.go
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"sort"
"strings"
"sync"

Expand All @@ -12,10 +13,20 @@ import (
)

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

aliasNameSourceSAToken = "sa_token"
aliasNameSourceSAPath = "sa_path"
aliasNameSourceDefault = aliasNameSourceSAToken
)

// map alias name source to its description
var aliasNameSourceMap = map[string]string{
aliasNameSourceSAToken: "format: <token.uid>",
aliasNameSourceSAPath: "format: <namespace>/<serviceaccount>",
}

// kubeAuthBackend implements logical.Backend
type kubeAuthBackend struct {
*framework.Backend
Expand Down Expand Up @@ -128,10 +139,39 @@ func (b *kubeAuthBackend) role(ctx context.Context, s logical.Storage, name stri
if len(role.TokenBoundCIDRs) == 0 && len(role.BoundCIDRs) > 0 {
role.TokenBoundCIDRs = role.BoundCIDRs
}
if role.AliasNameSource == "" {
role.AliasNameSource = aliasNameSourceDefault
}

return role, nil
}

func validateAliasNameSource(source string) error {
sources := make([]string, len(aliasNameSourceMap))
i := 0
calvn marked this conversation as resolved.
Show resolved Hide resolved
for s := range aliasNameSourceMap {
if s == source {
return nil
}
sources[i] = s
i++
}
sort.Strings(sources)
return fmt.Errorf(`invalid alias_name_source, must be one of: %s`, strings.Join(sources, ", "))
}

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

var backendHelp string = `
The Kubernetes Auth Backend allows authentication for Kubernetes service accounts.
`
17 changes: 16 additions & 1 deletion path_login.go
Expand Up @@ -95,11 +95,26 @@ func (b *kubeAuthBackend) pathLogin(ctx context.Context, req *logical.Request, d
return nil, errors.New("could not load backend configuration")
}

if err := validateAliasNameSource(role.AliasNameSource); err != nil {
calvn marked this conversation as resolved.
Show resolved Hide resolved
b.Logger().Error(err.Error())
return nil, err
}

serviceAccount, err := b.parseAndValidateJWT(jwtStr, role, config)
if err != nil {
return nil, err
}

var aliasName string
switch role.AliasNameSource {
case aliasNameSourceSAToken:
aliasName = serviceAccount.UID
calvn marked this conversation as resolved.
Show resolved Hide resolved
case aliasNameSourceSAPath:
aliasName = fmt.Sprintf("%s/%s", serviceAccount.Namespace, serviceAccount.Name)
default:
return nil, fmt.Errorf("unknown alias_name_source %q", role.AliasNameSource)
}

// look up the JWT token in the kubernetes API
err = serviceAccount.lookup(ctx, jwtStr, b.reviewFactory(config))
if err != nil {
Expand All @@ -109,7 +124,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
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
17 changes: 17 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 @@ -152,6 +157,8 @@ func (b *kubeAuthBackend) pathRoleRead(ctx context.Context, req *logical.Request
d["audience"] = role.Audience
}

d["alias_name_source"] = role.AliasNameSource

role.PopulateTokenData(d)

if len(role.Policies) > 0 {
Expand Down Expand Up @@ -302,6 +309,13 @@ 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)
}

// Store the entry.
entry, err := logical.StorageEntryJSON("role/"+strings.ToLower(roleName), role)
if err != nil {
Expand Down Expand Up @@ -332,6 +346,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
2 changes: 2 additions & 0 deletions path_role_test.go
Expand Up @@ -64,6 +64,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 +202,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