From 2cada2e73b8a6cb3c6d7e432bdda0294620eaeff Mon Sep 17 00:00:00 2001 From: Min Ni Date: Sun, 16 Jul 2023 10:47:29 -0700 Subject: [PATCH] Add DynamicBackendMode --- cmd/aws-iam-authenticator/root.go | 2 + pkg/config/types.go | 2 + pkg/fileutil/util.go | 107 +++++++++++ pkg/fileutil/util_test.go | 105 +++++++++++ pkg/mapper/configmap/configmap.go | 2 + pkg/mapper/dynamicfile/dynamicfile.go | 206 ++++++--------------- pkg/mapper/dynamicfile/dynamicfile_test.go | 79 ++++++-- pkg/mapper/dynamicfile/mapper.go | 6 +- pkg/server/server.go | 91 ++++++--- pkg/server/server_test.go | 126 +++++++++---- pkg/server/types.go | 11 +- 11 files changed, 506 insertions(+), 231 deletions(-) create mode 100644 pkg/fileutil/util.go create mode 100644 pkg/fileutil/util_test.go diff --git a/cmd/aws-iam-authenticator/root.go b/cmd/aws-iam-authenticator/root.go index 19f02654f..bad3e2d1e 100644 --- a/cmd/aws-iam-authenticator/root.go +++ b/cmd/aws-iam-authenticator/root.go @@ -108,6 +108,8 @@ func getConfig() (config.Config, error) { DynamicFilePath: viper.GetString("server.dynamicfilepath"), //DynamicFileUserIDStrict: if true, then aws UserId from sts will be used to look up the roleMapping/userMapping; or aws IdentityArn is used DynamicFileUserIDStrict: viper.GetBool("server.dynamicfileUserIDStrict"), + //DynamicBackendModePath: the file path containing the backend mode + DynamicBackendModePath: viper.GetString("server.dynamicBackendModePath"), } if err := viper.UnmarshalKey("server.mapRoles", &cfg.RoleMappings); err != nil { return cfg, fmt.Errorf("invalid server role mappings: %v", err) diff --git a/pkg/config/types.go b/pkg/config/types.go index f93f55ade..0b5e57486 100644 --- a/pkg/config/types.go +++ b/pkg/config/types.go @@ -154,6 +154,8 @@ type Config struct { DynamicFileUserIDStrict bool // ReservedPrefixConfig defines reserved username prefixes for each backend ReservedPrefixConfig map[string]ReservedPrefixConfig + // Dynamic File Path for BackendMode + DynamicBackendModePath string } type ReservedPrefixConfig struct { diff --git a/pkg/fileutil/util.go b/pkg/fileutil/util.go new file mode 100644 index 000000000..b5ca4badb --- /dev/null +++ b/pkg/fileutil/util.go @@ -0,0 +1,107 @@ +package fileutil + +import ( + "os" + "time" + + "github.com/fsnotify/fsnotify" + "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/util/wait" + "sigs.k8s.io/aws-iam-authenticator/pkg/metrics" +) + +type FileChangeCallBack interface { + CallBackForFileLoad(dynamicContent []byte) error + CallBackForFileDeletion() error +} + +func waitUntilFileAvailable(filename string, stopCh <-chan struct{}) { + if _, err := os.Stat(filename); err == nil { + return + } + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + for { + select { + case <-stopCh: + logrus.Infof("startLoadDynamicFile: waitUntilFileAvailable exit because get stopCh, filename is %s", filename) + return + case <-ticker.C: + if _, err := os.Stat(filename); err == nil { + return + } + } + } +} + +func loadDynamicFile(filename string, stopCh <-chan struct{}) ([]byte, error) { + waitUntilFileAvailable(filename, stopCh) + if content, err := os.ReadFile(filename); err == nil { + logrus.Infof("LoadDynamicFile: %v is available. content is %s", filename, string(content)) + return content, nil + } else { + return nil, err + } +} + +func StartLoadDynamicFile(filename string, callBack FileChangeCallBack, stopCh <-chan struct{}) { + go wait.Until(func() { + // start to watch the file change + watcher, err := fsnotify.NewWatcher() + if err != nil { + logrus.Errorf("startLoadDynamicFile: failed when call fsnotify.NewWatcher, %+v", err) + metrics.Get().DynamicFileFailures.Inc() + return + } + defer watcher.Close() + content, err := loadDynamicFile(filename, stopCh) + if err != nil { + logrus.Errorf("StartLoadDynamicFile: error in loadDynamicFile, %v", err) + return + } + err = watcher.Add(filename) + if err != nil { + logrus.Errorf("startLoadDynamicFile: could not add file to watcher %v", err) + metrics.Get().DynamicFileFailures.Inc() + return + } + if err := callBack.CallBackForFileLoad(content); err != nil { + logrus.Errorf("StartLoadDynamicFile: error in callBackForFileLoad, %v", err) + } + for { + select { + case <-stopCh: + logrus.Infof("startLoadDynamicFile: watching exit because stopCh closed, filename is %s", filename) + return + case event := <-watcher.Events: + switch { + case event.Op&fsnotify.Write == fsnotify.Write, event.Op&fsnotify.Create == fsnotify.Create: + // reload the access entry file + logrus.Info("startLoadDynamicFile: got WRITE/CREATE event reload it the memory") + content, err := loadDynamicFile(filename, stopCh) + if err != nil { + logrus.Errorf("StartLoadDynamicFile: error in loadDynamicFile, %v", err) + return + } + if err := callBack.CallBackForFileLoad(content); err != nil { + logrus.Errorf("StartLoadDynamicFile: error in callBackForFileLoad, %v", err) + } + case event.Op&fsnotify.Rename == fsnotify.Rename, event.Op&fsnotify.Remove == fsnotify.Remove: + logrus.Info("startLoadDynamicFile: got RENAME/REMOVE event") + // test if the "REMOVE" is triggered by vi or cp cmd + _, err := os.Stat(filename) + if os.IsNotExist(err) { + if err := callBack.CallBackForFileDeletion(); err != nil { + logrus.Errorf("StartLoadDynamicFile: error in callBackForFileDeletion, %v", err) + } + } + return + } + case err := <-watcher.Errors: + logrus.Errorf("startLoadDynamicFile: watcher.Errors for dynamic file %v", err) + metrics.Get().DynamicFileFailures.Inc() + return + } + } + }, time.Second, stopCh) +} diff --git a/pkg/fileutil/util_test.go b/pkg/fileutil/util_test.go new file mode 100644 index 000000000..e1a07c975 --- /dev/null +++ b/pkg/fileutil/util_test.go @@ -0,0 +1,105 @@ +package fileutil + +import ( + "os" + "sync" + "testing" + "time" +) + +var origFileContent = ` +Abbatxt7095 +` + +var updatedFileContent = ` +efgwht2033 +` + +type testStruct struct { + content string + expectedContent string + mutex sync.Mutex +} + +func (a *testStruct) CallBackForFileLoad(dynamicContent []byte) error { + a.mutex.Lock() + a.expectedContent = string(dynamicContent) + defer a.mutex.Unlock() + return nil +} + +func (a *testStruct) CallBackForFileDeletion() error { + a.mutex.Lock() + a.expectedContent = "" + defer a.mutex.Unlock() + return nil +} + +func TestLoadDynamicFile(t *testing.T) { + cases := []struct { + input string + want string + }{ + { + "abcde", + "abcde", + }, + { + "fghijk", + "fghijk", + }, + { + "xyzopq", + "xyzopq", + }, + { + "eks:test", + "eks:test", + }, + } + stopCh := make(chan struct{}) + testA := &testStruct{} + StartLoadDynamicFile("/tmp/util_test.txt", testA, stopCh) + defer close(stopCh) + time.Sleep(2 * time.Second) + os.WriteFile("/tmp/util_test.txt", []byte("test"), 0777) + for _, c := range cases { + updateFile(testA, c.input, t) + testA.mutex.Lock() + if testA.expectedContent != c.want { + t.Errorf( + "Unexpected result: TestLoadDynamicFile: got: %s, wanted %s", + testA.expectedContent, + c.want, + ) + } + testA.mutex.Unlock() + } +} + +func updateFile(testA *testStruct, origFileContent string, t *testing.T) { + testA.content = origFileContent + data := []byte(origFileContent) + err := os.WriteFile("/tmp/util_test.txt", data, 0600) + if err != nil { + t.Errorf("failed to create a local file /tmp/util_test.txt") + } + time.Sleep(1 * time.Second) +} + +func TestDeleteDynamicFile(t *testing.T) { + stopCh := make(chan struct{}) + testA := &testStruct{} + StartLoadDynamicFile("/tmp/delete.txt", testA, stopCh) + defer close(stopCh) + time.Sleep(2 * time.Second) + os.WriteFile("/tmp/delete.txt", []byte("test"), 0777) + time.Sleep(2 * time.Second) + os.Remove("/tmp/delete.txt") + time.Sleep(2 * time.Second) + testA.mutex.Lock() + if testA.expectedContent != "" { + t.Errorf("failed in TestDeleteDynamicFile") + } + testA.mutex.Unlock() +} diff --git a/pkg/mapper/configmap/configmap.go b/pkg/mapper/configmap/configmap.go index d77790057..63aa4d79f 100644 --- a/pkg/mapper/configmap/configmap.go +++ b/pkg/mapper/configmap/configmap.go @@ -52,9 +52,11 @@ func New(masterURL, kubeConfig string) (*MapStore, error) { // when the values change. func (ms *MapStore) startLoadConfigMap(stopCh <-chan struct{}) { go func() { + logrus.Info("startLoadConfigMap in EKSConfigMap") for { select { case <-stopCh: + logrus.Info("stopCh is closed in startLoadConfigMap") return default: watcher, err := ms.configMap.Watch(context.TODO(), metav1.ListOptions{ diff --git a/pkg/mapper/dynamicfile/dynamicfile.go b/pkg/mapper/dynamicfile/dynamicfile.go index d3430c442..6dacf8822 100644 --- a/pkg/mapper/dynamicfile/dynamicfile.go +++ b/pkg/mapper/dynamicfile/dynamicfile.go @@ -4,16 +4,12 @@ import ( "encoding/json" "errors" "fmt" - "github.com/fsnotify/fsnotify" + "strings" + "sync" + "github.com/sirupsen/logrus" - "k8s.io/apimachinery/pkg/util/wait" - "os" "sigs.k8s.io/aws-iam-authenticator/pkg/arn" "sigs.k8s.io/aws-iam-authenticator/pkg/config" - "sigs.k8s.io/aws-iam-authenticator/pkg/metrics" - "strings" - "sync" - "time" ) type DynamicFileMapStore struct { @@ -48,36 +44,6 @@ func (err ErrParsingMap) Error() string { return fmt.Sprintf("error parsing dynamic file: %v", err.errors) } -func waitUntilFileAvailable(filename string) error { - for { - _, err := os.Stat(filename) - if os.IsNotExist(err) { - time.Sleep(1 * time.Second) - continue - } else { - return err - } - } -} - -func (m *DynamicFileMapStore) loadDynamicFile() error { - err := waitUntilFileAvailable(m.filename) - if err != nil { - logrus.Errorf("LoadDynamicFile: failed to wait till dynamic file available %v", err) - return err - } - logrus.Infof("LoadDynamicFile: %v is available. loading", m.filename) - // load the initial file content into memory - userMappings, roleMappings, awsAccounts, err := ParseMap(m) - if err != nil { - logrus.Errorf("LoadDynamicFile: There was an error parsing the dynamic file: %+v. Map is not updated. Please correct dynamic file", err) - return err - } else { - m.saveMap(userMappings, roleMappings, awsAccounts) - } - return nil -} - func NewDynamicFileMapStore(cfg config.Config) (*DynamicFileMapStore, error) { ms := DynamicFileMapStore{} ms.filename = cfg.DynamicFilePath @@ -85,115 +51,6 @@ func NewDynamicFileMapStore(cfg config.Config) (*DynamicFileMapStore, error) { return &ms, nil } -func (m *DynamicFileMapStore) startLoadDynamicFile(stopCh <-chan struct{}) { - go wait.Until(func() { - err := m.loadDynamicFile() - if err != nil { - logrus.Errorf("startLoadDynamicFile: failed when loadDynamicFile, %+v", err) - metrics.Get().DynamicFileFailures.Inc() - return - } - // start to watch the file change - watcher, err := fsnotify.NewWatcher() - if err != nil { - logrus.Errorf("startLoadDynamicFile: failed when call fsnotify.NewWatcher, %+v", err) - metrics.Get().DynamicFileFailures.Inc() - return - } - err = watcher.Add(m.filename) - if err != nil { - logrus.Errorf("startLoadDynamicFile: could not add file to watcher %v", err) - metrics.Get().DynamicFileFailures.Inc() - return - } - - defer watcher.Close() - for { - select { - case <-stopCh: - return - case event := <-watcher.Events: - switch { - case event.Op&fsnotify.Write == fsnotify.Write, event.Op&fsnotify.Create == fsnotify.Create: - // reload the access entry file - logrus.Info("startLoadDynamicFile: got WRITE/CREATE event reload it the memory") - m.loadDynamicFile() - case event.Op&fsnotify.Rename == fsnotify.Rename, event.Op&fsnotify.Remove == fsnotify.Remove: - logrus.Info("startLoadDynamicFile: got RENAME/REMOVE event") - // test if the "REMOVE" is triggered by vi or cp cmd - _, err := os.Stat(m.filename) - if os.IsNotExist(err) { - // the "REMOVE" event is not triggered by vi or cp cmd - // reset memory - userMappings := make([]config.UserMapping, 0) - roleMappings := make([]config.RoleMapping, 0) - awsAccounts := make([]string, 0) - m.saveMap(userMappings, roleMappings, awsAccounts) - } - return - } - case err := <-watcher.Errors: - logrus.Errorf("startLoadDynamicFile: watcher.Errors for dynamic file %v", err) - metrics.Get().DynamicFileFailures.Inc() - return - } - } - }, time.Second, stopCh) -} - -func ParseMap(m *DynamicFileMapStore) (userMappings []config.UserMapping, roleMappings []config.RoleMapping, awsAccounts []string, err error) { - errs := make([]error, 0) - userMappings = make([]config.UserMapping, 0) - roleMappings = make([]config.RoleMapping, 0) - filename := m.filename - dynamicContent, err := os.ReadFile(filename) - if err != nil { - logrus.Errorf("ParseMap: could not read from dynamic file") - return userMappings, roleMappings, awsAccounts, err - } - - var dynamicFileData DynamicFileData - err = json.Unmarshal([]byte(dynamicContent), &dynamicFileData) - if err != nil { - if len(dynamicContent) == 0 { - return userMappings, roleMappings, awsAccounts, nil - } - logrus.Error("ParseMap: could not unmarshal dynamic file.") - return userMappings, roleMappings, awsAccounts, err - } - - for _, userMapping := range dynamicFileData.UserMappings { - key := userMapping.UserARN - if m.userIDStrict { - key = userMapping.UserId - } - if key == "" { - errs = append(errs, fmt.Errorf("Value for userarn or userid(if dynamicfileUserIDStrict = true) must be supplied")) - } else { - userMappings = append(userMappings, userMapping) - } - } - - for _, roleMapping := range dynamicFileData.RoleMappings { - key := roleMapping.RoleARN - if m.userIDStrict { - key = roleMapping.UserId - } - if key == "" { - errs = append(errs, fmt.Errorf("Value for rolearn or userid(if dynamicfileUserIDStrict = true) must be supplied")) - } else { - roleMappings = append(roleMappings, roleMapping) - } - } - - awsAccounts = dynamicFileData.AutoMappedAWSAccounts[:] - - if len(errs) > 0 { - logrus.Warnf("ParseMap: Errors parsing dynamic file: %+v", errs) - err = ErrParsingMap{errors: errs} - } - return userMappings, roleMappings, awsAccounts, err -} func (ms *DynamicFileMapStore) saveMap( userMappings []config.UserMapping, roleMappings []config.RoleMapping, @@ -270,3 +127,60 @@ func (ms *DynamicFileMapStore) LogMapping() { logrus.Info(awsAccount) } } + +func (ms *DynamicFileMapStore) CallBackForFileLoad(dynamicContent []byte) error { + errs := make([]error, 0) + userMappings := make([]config.UserMapping, 0) + roleMappings := make([]config.RoleMapping, 0) + var dynamicFileData DynamicFileData + err := json.Unmarshal([]byte(dynamicContent), &dynamicFileData) + if err != nil { + //if len(dynamicContent) == 0 { + // return userMappings, roleMappings, awsAccounts, nil + //} + logrus.Error("ParseMap: could not unmarshal dynamic file.") + return err + } + + for _, userMapping := range dynamicFileData.UserMappings { + key := userMapping.UserARN + if ms.userIDStrict { + key = userMapping.UserId + } + if key == "" { + errs = append(errs, fmt.Errorf("Value for userarn or userid(if dynamicfileUserIDStrict = true) must be supplied")) + } else { + userMappings = append(userMappings, userMapping) + } + } + + for _, roleMapping := range dynamicFileData.RoleMappings { + key := roleMapping.RoleARN + if ms.userIDStrict { + key = roleMapping.UserId + } + if key == "" { + errs = append(errs, fmt.Errorf("Value for rolearn or userid(if dynamicfileUserIDStrict = true) must be supplied")) + } else { + roleMappings = append(roleMappings, roleMapping) + } + } + + awsAccounts := dynamicFileData.AutoMappedAWSAccounts[:] + + if len(errs) > 0 { + logrus.Warnf("ParseMap: Errors parsing dynamic file: %+v", errs) + err = ErrParsingMap{errors: errs} + return err + } + ms.saveMap(userMappings, roleMappings, awsAccounts) + return nil +} + +func (ms *DynamicFileMapStore) CallBackForFileDeletion() error { + userMappings := make([]config.UserMapping, 0) + roleMappings := make([]config.RoleMapping, 0) + awsAccounts := make([]string, 0) + ms.saveMap(userMappings, roleMappings, awsAccounts) + return nil +} diff --git a/pkg/mapper/dynamicfile/dynamicfile_test.go b/pkg/mapper/dynamicfile/dynamicfile_test.go index 3691b20b6..9c1df591d 100644 --- a/pkg/mapper/dynamicfile/dynamicfile_test.go +++ b/pkg/mapper/dynamicfile/dynamicfile_test.go @@ -7,6 +7,7 @@ import ( "time" "sigs.k8s.io/aws-iam-authenticator/pkg/config" + "sigs.k8s.io/aws-iam-authenticator/pkg/fileutil" ) var ( @@ -185,7 +186,7 @@ func TestUserIdStrict(t *testing.T) { if err != nil { t.Errorf("failed to create a local file /tmp/test.txt") } - ms.startLoadDynamicFile(stopCh) + fileutil.StartLoadDynamicFile(ms.filename, ms, stopCh) time.Sleep(1 * time.Second) ms.mutex.RLock() for key, _ := range ms.roles { @@ -216,7 +217,7 @@ func TestWithoutUserIdStrict(t *testing.T) { if err != nil { t.Errorf("failed to create a local file /tmp/test.txt") } - ms.startLoadDynamicFile(stopCh) + fileutil.StartLoadDynamicFile(ms.filename, ms, stopCh) time.Sleep(1 * time.Second) ms.mutex.RLock() for key, _ := range ms.roles { @@ -229,7 +230,7 @@ func TestWithoutUserIdStrict(t *testing.T) { defer os.Remove("/tmp/test.txt") } -func TestLoadDynamicFile(t *testing.T) { +func TestLoadDynamicFileMode(t *testing.T) { stopCh := make(chan struct{}) defer close(stopCh) @@ -243,7 +244,7 @@ func TestLoadDynamicFile(t *testing.T) { t.Errorf("failed to create a DynamicFileMapper") } - ms.startLoadDynamicFile(stopCh) + fileutil.StartLoadDynamicFile(ms.filename, ms, stopCh) time.Sleep(1 * time.Second) ms.mutex.RLock() if len(ms.roles) != 0 { @@ -289,11 +290,10 @@ func TestLoadDynamicFile(t *testing.T) { if err != nil { t.Errorf("failed to create expected DynamicFileMapper") } - expectedUserMappings, expectedRoleMappings, expectedAwsAccounts, err := ParseMap(expectedMapStore) + err = expectedMapStore.CallBackForFileLoad([]byte(updatedFileContent)) if err != nil { t.Errorf("failed to ParseMap expected DynamicFileMapper") } - expectedMapStore.saveMap(expectedUserMappings, expectedRoleMappings, expectedAwsAccounts) time.Sleep(1 * time.Second) @@ -358,13 +358,37 @@ func TestLoadDynamicFile(t *testing.T) { } -func TestParseMap(t *testing.T) { +func TestCallBackForFileDeletion(t *testing.T) { + cfg := config.Config{ + DynamicFileUserIDStrict: true, + DynamicFilePath: "/tmp/test.txt", + } + ms, err := NewDynamicFileMapStore(cfg) + if err != nil { + t.Errorf("failed to create a DynamicFileMapper") + } - data := []byte(origFileContent) - err := os.WriteFile("/tmp/test.txt", data, 0600) + err = ms.CallBackForFileDeletion() if err != nil { - t.Errorf("failed to create a local file /tmp/test.txt") + t.Fatal(err) + } + + if len(ms.users) != 0 { + t.Fatalf("unexpected userMappings %+v", ms.users) } + + if len(ms.roles) != 0 { + t.Fatalf("unexpected userMappings %+v", ms.roles) + } + + if len(ms.awsAccounts) != 0 { + t.Fatalf("unexpected userMappings %+v", ms.awsAccounts) + } +} + +func TestCallBackForFileLoad(t *testing.T) { + + data := []byte(origFileContent) cfg := config.Config{ DynamicFileUserIDStrict: true, DynamicFilePath: "/tmp/test.txt", @@ -374,7 +398,7 @@ func TestParseMap(t *testing.T) { t.Errorf("failed to create a DynamicFileMapper") } - u, r, a, err := ParseMap(ms) + err = ms.CallBackForFileLoad(data) if err != nil { t.Fatal(err) } @@ -393,16 +417,33 @@ func TestParseMap(t *testing.T) { } origAccounts := []string{"012345678901", "456789012345"} - if !reflect.DeepEqual(u, origUserMappings) { - t.Fatalf("unexpected userMappings %+v", u) + if len(ms.users) != len(origUserMappings) { + t.Fatalf("unexpected userMappings %+v", ms.users) } - if !reflect.DeepEqual(r, origRoleMappings) { - t.Fatalf("unexpected roleMappings %+v", r) + + for _, user := range origUserMappings { + if _, ok := ms.users[user.UserId]; !ok { + t.Fatalf("unexpected userMappings %+v", ms.users) + } } - if !reflect.DeepEqual(a, origAccounts) { - t.Fatalf("unexpected accounts %+v", a) + + if len(ms.roles) != len(origRoleMappings) { + t.Fatalf("unexpected userMappings %+v", ms.roles) + } + + for _, role := range origRoleMappings { + if _, ok := ms.roles[role.UserId]; !ok { + t.Fatalf("unexpected userMappings %+v", ms.roles) + } } - //clean testing files - defer os.Remove("/tmp/test.txt") + if len(ms.awsAccounts) != len(origAccounts) { + t.Fatalf("unexpected userMappings %+v", ms.awsAccounts) + } + + for _, account := range origAccounts { + if _, ok := ms.awsAccounts[account]; !ok { + t.Fatalf("unexpected userMappings %+v", ms.users) + } + } } diff --git a/pkg/mapper/dynamicfile/mapper.go b/pkg/mapper/dynamicfile/mapper.go index 6d842f547..dd2090d07 100644 --- a/pkg/mapper/dynamicfile/mapper.go +++ b/pkg/mapper/dynamicfile/mapper.go @@ -1,10 +1,12 @@ package dynamicfile import ( + "strings" + "sigs.k8s.io/aws-iam-authenticator/pkg/config" + "sigs.k8s.io/aws-iam-authenticator/pkg/fileutil" "sigs.k8s.io/aws-iam-authenticator/pkg/mapper" "sigs.k8s.io/aws-iam-authenticator/pkg/token" - "strings" ) type DynamicFileMapper struct { @@ -29,7 +31,7 @@ func (m *DynamicFileMapper) Name() string { } func (m *DynamicFileMapper) Start(stopCh <-chan struct{}) error { - m.startLoadDynamicFile(stopCh) + fileutil.StartLoadDynamicFile(m.filename, m.DynamicFileMapStore, stopCh) return nil } diff --git a/pkg/server/server.go b/pkg/server/server.go index 7d1e36c34..d844cc199 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -25,12 +25,14 @@ import ( "net/http" "regexp" "strings" + "sync" "time" "github.com/aws/aws-sdk-go/aws/ec2metadata" "github.com/aws/aws-sdk-go/aws/session" "sigs.k8s.io/aws-iam-authenticator/pkg/config" "sigs.k8s.io/aws-iam-authenticator/pkg/ec2provider" + "sigs.k8s.io/aws-iam-authenticator/pkg/fileutil" "sigs.k8s.io/aws-iam-authenticator/pkg/mapper" "sigs.k8s.io/aws-iam-authenticator/pkg/mapper/configmap" "sigs.k8s.io/aws-iam-authenticator/pkg/mapper/crd" @@ -67,11 +69,13 @@ var ( // server state (internal) type handler struct { http.ServeMux + mutex sync.RWMutex verifier token.Verifier ec2Provider ec2provider.EC2Provider clusterID string - mappers []mapper.Mapper + backendMapper BackendMapper scrubbedAccounts []string + cfg config.Config } // New authentication webhook server. @@ -80,18 +84,11 @@ func New(cfg config.Config, stopCh <-chan struct{}) *Server { Config: cfg, } - mappers, err := BuildMapperChain(cfg) + backendMapper, err := BuildMapperChain(cfg, cfg.BackendMode) if err != nil { logrus.Fatalf("failed to build mapper chain: %v", err) } - for _, m := range mappers { - logrus.Infof("starting mapper %q", m.Name()) - if err := m.Start(stopCh); err != nil { - logrus.Fatalf("start mapper %q failed", m.Name()) - } - } - for _, mapping := range c.RoleMappings { logrus.WithFields(logrus.Fields{ "role": mapping.RoleARN, @@ -137,11 +134,13 @@ func New(cfg config.Config, stopCh <-chan struct{}) *Server { logrus.Infof("listening on %s", listener.Addr()) logrus.Infof("reconfigure your apiserver with `--authentication-token-webhook-config-file=%s` to enable (assuming default hostPath mounts)", c.GenerateKubeconfigPath) + internalHandler := c.getHandler(backendMapper, c.EC2DescribeInstancesQps, c.EC2DescribeInstancesBurst, stopCh) c.httpServer = http.Server{ ErrorLog: log.New(errLog, "", 0), - Handler: c.getHandler(mappers, c.EC2DescribeInstancesQps, c.EC2DescribeInstancesBurst), + Handler: internalHandler, } c.listener = listener + c.internalHandler = internalHandler return c } @@ -153,6 +152,16 @@ func (c *Server) Run(stopCh <-chan struct{}) { go func() { http.ListenAndServe(":21363", &healthzHandler{}) }() + go func() { + for { + select { + case <-stopCh: + logrus.Info("shut down mapper before return from Run") + close(c.internalHandler.backendMapper.mapperStopCh) + return + } + } + }() if err := c.httpServer.Serve(c.listener); err != nil { logrus.WithError(err).Warning("http server exited") } @@ -172,7 +181,7 @@ type healthzHandler struct{} func (m *healthzHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "ok") } -func (c *Server) getHandler(mappers []mapper.Mapper, ec2DescribeQps int, ec2DescribeBurst int) *handler { +func (c *Server) getHandler(backendMapper BackendMapper, ec2DescribeQps int, ec2DescribeBurst int, stopCh <-chan struct{}) *handler { if c.ServerEC2DescribeInstancesRoleARN != "" { _, err := awsarn.Parse(c.ServerEC2DescribeInstancesRoleARN) if err != nil { @@ -190,8 +199,9 @@ func (c *Server) getHandler(mappers []mapper.Mapper, ec2DescribeQps int, ec2Desc verifier: token.NewVerifier(c.ClusterID, c.PartitionID, instanceRegion), ec2Provider: ec2provider.New(c.ServerEC2DescribeInstancesRoleARN, instanceRegion, ec2DescribeQps, ec2DescribeBurst), clusterID: c.ClusterID, - mappers: mappers, + backendMapper: backendMapper, scrubbedAccounts: c.Config.ScrubbedAWSAccounts, + cfg: c.Config, } h.HandleFunc("/authenticate", h.authenticateEndpoint) @@ -201,12 +211,18 @@ func (c *Server) getHandler(mappers []mapper.Mapper, ec2DescribeQps int, ec2Desc }) logrus.Infof("Starting the h.ec2Provider.startEc2DescribeBatchProcessing ") go h.ec2Provider.StartEc2DescribeBatchProcessing() + if strings.TrimSpace(c.DynamicBackendModePath) != "" { + fileutil.StartLoadDynamicFile(c.DynamicBackendModePath, h, stopCh) + } + return h } -func BuildMapperChain(cfg config.Config) ([]mapper.Mapper, error) { - modes := cfg.BackendMode - mappers := []mapper.Mapper{} +func BuildMapperChain(cfg config.Config, modes []string) (BackendMapper, error) { + backendMapper := BackendMapper{ + mappers: []mapper.Mapper{}, + mapperStopCh: make(chan struct{}), + } for _, mode := range modes { switch mode { case mapper.ModeFile: @@ -214,34 +230,40 @@ func BuildMapperChain(cfg config.Config) ([]mapper.Mapper, error) { case mapper.ModeMountedFile: fileMapper, err := file.NewFileMapper(cfg) if err != nil { - return nil, fmt.Errorf("backend-mode %q creation failed: %v", mode, err) + return BackendMapper{}, fmt.Errorf("backend-mode %q creation failed: %v", mode, err) } - mappers = append(mappers, fileMapper) + backendMapper.mappers = append(backendMapper.mappers, fileMapper) case mapper.ModeConfigMap: fallthrough case mapper.ModeEKSConfigMap: configMapMapper, err := configmap.NewConfigMapMapper(cfg) if err != nil { - return nil, fmt.Errorf("backend-mode %q creation failed: %v", mode, err) + return BackendMapper{}, fmt.Errorf("backend-mode %q creation failed: %v", mode, err) } - mappers = append(mappers, configMapMapper) + backendMapper.mappers = append(backendMapper.mappers, configMapMapper) case mapper.ModeCRD: crdMapper, err := crd.NewCRDMapper(cfg) if err != nil { - return nil, fmt.Errorf("backend-mode %q creation failed: %v", mode, err) + return BackendMapper{}, fmt.Errorf("backend-mode %q creation failed: %v", mode, err) } - mappers = append(mappers, crdMapper) + backendMapper.mappers = append(backendMapper.mappers, crdMapper) case mapper.ModeDynamicFile: dynamicFileMapper, err := dynamicfile.NewDynamicFileMapper(cfg) if err != nil { - return nil, fmt.Errorf("backend-mode %q creation failed: %v", mode, err) + return BackendMapper{}, fmt.Errorf("backend-mode %q creation failed: %v", mode, err) } - mappers = append(mappers, dynamicFileMapper) + backendMapper.mappers = append(backendMapper.mappers, dynamicFileMapper) default: - return nil, fmt.Errorf("backend-mode %q is not a valid mode", mode) + return BackendMapper{}, fmt.Errorf("backend-mode %q is not a valid mode", mode) + } + } + for _, m := range backendMapper.mappers { + logrus.Infof("starting mapper %q", m.Name()) + if err := m.Start(backendMapper.mapperStopCh); err != nil { + logrus.Fatalf("start mapper %q failed", m.Name()) } } - return mappers, nil + return backendMapper, nil } func duration(start time.Time) float64 { @@ -377,7 +399,7 @@ func ReservedPrefixExists(username string, reservedList []string) bool { func (h *handler) doMapping(identity *token.Identity) (string, []string, error) { var errs []error - for _, m := range h.mappers { + for _, m := range h.backendMapper.mappers { mapping, err := m.Map(identity) if err == nil { // Mapping found, try to render any templates like {{EC2PrivateDNSName}} @@ -449,3 +471,20 @@ func (h *handler) renderTemplate(template string, identity *token.Identity) (str return template, nil } + +func (h *handler) CallBackForFileLoad(dynamicContent []byte) error { + newMapper, err := BuildMapperChain(h.cfg, strings.Split(string(dynamicContent), ",")) + if err == nil && len(newMapper.mappers) > 0 { + // replace the mapper + close(h.backendMapper.mapperStopCh) + h.backendMapper = newMapper + } else { + logrus.Errorf("Error CallBackForFileLoad: failed when BuildMapperChain, %v", err) + } + return nil +} + +func (h *handler) CallBackForFileDeletion() error { + logrus.Infof("BackendMode dynamic file got deleted") + return nil +} diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go index d36057b38..eb2ce541e 100644 --- a/pkg/server/server_test.go +++ b/pkg/server/server_test.go @@ -481,13 +481,16 @@ func TestAuthenticateVerifierRoleMapping(t *testing.T) { AccessKeyID: "ABCDEF", } h := setup(&testVerifier{err: nil, identity: identity}) - h.mappers = []mapper.Mapper{file.NewFileMapperWithMaps(map[string]config.RoleMapping{ - "arn:aws:iam::0123456789012:role/test": config.RoleMapping{ - RoleARN: "arn:aws:iam::0123456789012:role/Test", - Username: "TestUser", - Groups: []string{"sys:admin", "listers"}, - }, - }, nil, nil)} + h.backendMapper = BackendMapper{ + mappers: []mapper.Mapper{file.NewFileMapperWithMaps(map[string]config.RoleMapping{ + "arn:aws:iam::0123456789012:role/test": config.RoleMapping{ + RoleARN: "arn:aws:iam::0123456789012:role/Test", + Username: "TestUser", + Groups: []string{"sys:admin", "listers"}, + }, + }, nil, nil)}, + mapperStopCh: make(chan struct{}), + } h.authenticateEndpoint(resp, req) if resp.Code != http.StatusOK { t.Errorf("Expected status code %d, was %d", http.StatusOK, resp.Code) @@ -527,7 +530,10 @@ func TestAuthenticateVerifierRoleMappingCRD(t *testing.T) { }}) indexer := createIndexer() indexer.Add(newIAMIdentityMapping("arn:aws:iam::0123456789012:role/Test", "arn:aws:iam::0123456789012:role/test", "TestUser", []string{"sys:admin", "listers"})) - h.mappers = []mapper.Mapper{crd.NewCRDMapperWithIndexer(indexer)} + h.backendMapper = BackendMapper{ + mappers: []mapper.Mapper{crd.NewCRDMapperWithIndexer(indexer)}, + mapperStopCh: make(chan struct{}), + } h.authenticateEndpoint(resp, req) if resp.Code != http.StatusOK { t.Errorf("Expected status code %d, was %d", http.StatusOK, resp.Code) @@ -565,13 +571,16 @@ func TestAuthenticateVerifierUserMapping(t *testing.T) { UserID: "Test", SessionName: "TestSession", }}) - h.mappers = []mapper.Mapper{file.NewFileMapperWithMaps(nil, map[string]config.UserMapping{ - "arn:aws:iam::0123456789012:user/test": config.UserMapping{ - UserARN: "arn:aws:iam::0123456789012:user/Test", - Username: "TestUser", - Groups: []string{"sys:admin", "listers"}, - }, - }, nil)} + h.backendMapper = BackendMapper{ + mappers: []mapper.Mapper{file.NewFileMapperWithMaps(nil, map[string]config.UserMapping{ + "arn:aws:iam::0123456789012:user/test": config.UserMapping{ + UserARN: "arn:aws:iam::0123456789012:user/Test", + Username: "TestUser", + Groups: []string{"sys:admin", "listers"}, + }, + }, nil)}, + mapperStopCh: make(chan struct{}), + } h.authenticateEndpoint(resp, req) if resp.Code != http.StatusOK { t.Errorf("Expected status code %d, was %d", http.StatusOK, resp.Code) @@ -611,7 +620,10 @@ func TestAuthenticateVerifierUserMappingCRD(t *testing.T) { }}) indexer := createIndexer() indexer.Add(newIAMIdentityMapping("arn:aws:iam::0123456789012:user/Test", "arn:aws:iam::0123456789012:user/test", "TestUser", []string{"sys:admin", "listers"})) - h.mappers = []mapper.Mapper{crd.NewCRDMapperWithIndexer(indexer)} + h.backendMapper = BackendMapper{ + mappers: []mapper.Mapper{crd.NewCRDMapperWithIndexer(indexer)}, + mapperStopCh: make(chan struct{}), + } h.authenticateEndpoint(resp, req) if resp.Code != http.StatusOK { t.Errorf("Expected status code %d, was %d", http.StatusOK, resp.Code) @@ -649,9 +661,12 @@ func TestAuthenticateVerifierAccountMappingForUser(t *testing.T) { UserID: "Test", SessionName: "TestSession", }}) - h.mappers = []mapper.Mapper{file.NewFileMapperWithMaps(nil, nil, map[string]bool{ - "0123456789012": true, - })} + h.backendMapper = BackendMapper{ + mappers: []mapper.Mapper{file.NewFileMapperWithMaps(nil, nil, map[string]bool{ + "0123456789012": true, + })}, + mapperStopCh: make(chan struct{}), + } h.authenticateEndpoint(resp, req) if resp.Code != http.StatusOK { t.Errorf("Expected status code %d, was %d", http.StatusOK, resp.Code) @@ -689,9 +704,12 @@ func TestAuthenticateVerifierAccountMappingForUserCRD(t *testing.T) { UserID: "Test", SessionName: "TestSession", }}) - h.mappers = []mapper.Mapper{crd.NewCRDMapperWithIndexer(createIndexer()), file.NewFileMapperWithMaps(nil, nil, map[string]bool{ - "0123456789012": true, - })} + h.backendMapper = BackendMapper{ + mappers: []mapper.Mapper{crd.NewCRDMapperWithIndexer(createIndexer()), file.NewFileMapperWithMaps(nil, nil, map[string]bool{ + "0123456789012": true, + })}, + mapperStopCh: make(chan struct{}), + } h.authenticateEndpoint(resp, req) if resp.Code != http.StatusOK { t.Errorf("Expected status code %d, was %d", http.StatusOK, resp.Code) @@ -729,9 +747,12 @@ func TestAuthenticateVerifierAccountMappingForRole(t *testing.T) { UserID: "Test", SessionName: "TestSession", }}) - h.mappers = []mapper.Mapper{file.NewFileMapperWithMaps(nil, nil, map[string]bool{ - "0123456789012": true, - })} + h.backendMapper = BackendMapper{ + mappers: []mapper.Mapper{file.NewFileMapperWithMaps(nil, nil, map[string]bool{ + "0123456789012": true, + })}, + mapperStopCh: make(chan struct{}), + } h.authenticateEndpoint(resp, req) if resp.Code != http.StatusOK { t.Errorf("Expected status code %d, was %d", http.StatusOK, resp.Code) @@ -769,9 +790,12 @@ func TestAuthenticateVerifierAccountMappingForRoleCRD(t *testing.T) { UserID: "Test", SessionName: "TestSession", }}) - h.mappers = []mapper.Mapper{crd.NewCRDMapperWithIndexer(createIndexer()), file.NewFileMapperWithMaps(nil, nil, map[string]bool{ - "0123456789012": true, - })} + h.backendMapper = BackendMapper{ + mappers: []mapper.Mapper{crd.NewCRDMapperWithIndexer(createIndexer()), file.NewFileMapperWithMaps(nil, nil, map[string]bool{ + "0123456789012": true, + })}, + mapperStopCh: make(chan struct{}), + } h.authenticateEndpoint(resp, req) if resp.Code != http.StatusOK { t.Errorf("Expected status code %d, was %d", http.StatusOK, resp.Code) @@ -810,13 +834,16 @@ func TestAuthenticateVerifierNodeMapping(t *testing.T) { SessionName: "i-0c6f21bf1f24f9708", }}) h.ec2Provider = newTestEC2Provider("ip-172-31-27-14", 15, 5) - h.mappers = []mapper.Mapper{file.NewFileMapperWithMaps(map[string]config.RoleMapping{ - "arn:aws:iam::0123456789012:role/testnoderole": config.RoleMapping{ - RoleARN: "arn:aws:iam::0123456789012:role/TestNodeRole", - Username: "system:node:{{EC2PrivateDNSName}}", - Groups: []string{"system:nodes", "system:bootstrappers"}, - }, - }, nil, nil)} + h.backendMapper = BackendMapper{ + mappers: []mapper.Mapper{file.NewFileMapperWithMaps(map[string]config.RoleMapping{ + "arn:aws:iam::0123456789012:role/testnoderole": config.RoleMapping{ + RoleARN: "arn:aws:iam::0123456789012:role/TestNodeRole", + Username: "system:node:{{EC2PrivateDNSName}}", + Groups: []string{"system:nodes", "system:bootstrappers"}, + }, + }, nil, nil)}, + mapperStopCh: make(chan struct{}), + } h.authenticateEndpoint(resp, req) if resp.Code != http.StatusOK { t.Errorf("Expected status code %d, was %d", http.StatusOK, resp.Code) @@ -858,7 +885,10 @@ func TestAuthenticateVerifierNodeMappingCRD(t *testing.T) { h.ec2Provider = newTestEC2Provider("ip-172-31-27-14", 15, 5) indexer := createIndexer() indexer.Add(newIAMIdentityMapping("arn:aws:iam::0123456789012:role/TestNodeRole", "arn:aws:iam::0123456789012:role/testnoderole", "system:node:{{EC2PrivateDNSName}}", []string{"system:nodes", "system:bootstrappers"})) - h.mappers = []mapper.Mapper{crd.NewCRDMapperWithIndexer(indexer)} + h.backendMapper = BackendMapper{ + mappers: []mapper.Mapper{crd.NewCRDMapperWithIndexer(indexer)}, + mapperStopCh: make(chan struct{}), + } h.authenticateEndpoint(resp, req) if resp.Code != http.StatusOK { t.Errorf("Expected status code %d, was %d", http.StatusOK, resp.Code) @@ -964,3 +994,27 @@ func TestRenderTemplate(t *testing.T) { }) } } + +func TestCallBackForFileLoad(t *testing.T) { + fileContent := strings.Split(string("DynamicFile,MountedFile"), ",") + + cfg := config.Config{ + DynamicFilePath: "/tmp/server_test.txt", + } + h := &handler{ + cfg: cfg, + } + newMapper, err := BuildMapperChain(h.cfg, fileContent) + if err != nil { + t.Errorf("Fail in TestCallBackForFileLoad: BuildMapperChain") + } + if len(newMapper.mappers) != len(fileContent) { + t.Errorf("Fail in TestCallBackForFileLoad: unpected mapper length") + } + if newMapper.mappers[0].Name() != "DynamicFile" { + t.Errorf("Fail in TestCallBackForFileLoad: unpected mapper mode") + } + if newMapper.mappers[1].Name() != "MountedFile" { + t.Errorf("Fail in TestCallBackForFileLoad: unpected mapper mode") + } +} diff --git a/pkg/server/types.go b/pkg/server/types.go index ec6d3b11f..445295d17 100644 --- a/pkg/server/types.go +++ b/pkg/server/types.go @@ -21,12 +21,19 @@ import ( "net/http" "sigs.k8s.io/aws-iam-authenticator/pkg/config" + "sigs.k8s.io/aws-iam-authenticator/pkg/mapper" ) // Server for the authentication webhook. type Server struct { // Config is the whole configuration of aws-iam-authenticator used for valid keys and certs, kubeconfig, and so on config.Config - httpServer http.Server - listener net.Listener + httpServer http.Server + listener net.Listener + internalHandler *handler +} + +type BackendMapper struct { + mappers []mapper.Mapper + mapperStopCh chan struct{} }