Skip to content

Commit

Permalink
Add new backend mode DYNAMICFILE
Browse files Browse the repository at this point in the history
  • Loading branch information
nnmin-aws committed Oct 1, 2022
1 parent f15f0fd commit ce51f94
Show file tree
Hide file tree
Showing 8 changed files with 659 additions and 4 deletions.
15 changes: 15 additions & 0 deletions cmd/aws-iam-authenticator/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ func getConfig() (config.Config, error) {
EC2DescribeInstancesQps: viper.GetInt("server.ec2DescribeInstancesQps"),
EC2DescribeInstancesBurst: viper.GetInt("server.ec2DescribeInstancesBurst"),
ScrubbedAWSAccounts: viper.GetStringSlice("server.scrubbedAccounts"),
DynamicFilePath: viper.GetString("server.dynamicfilepath"),
}
if err := viper.UnmarshalKey("server.mapRoles", &cfg.RoleMappings); err != nil {
return cfg, fmt.Errorf("invalid server role mappings: %v", err)
Expand Down Expand Up @@ -135,6 +136,20 @@ func getConfig() (config.Config, error) {
return cfg, errors.New("Invalid partition")
}

// DynamicFile BackendMode and DynamicFilePath are mutually inclusive.
var dynamicFileModeSet bool
for _, mode := range cfg.BackendMode {
if mode == mapper.ModeDynamicFile {
dynamicFileModeSet = true
}
}
if dynamicFileModeSet && cfg.DynamicFilePath == "" {
logrus.Fatal("dynamicfile is set in backend-mode but dynamicfilepath is not set")
}
if !dynamicFileModeSet && cfg.DynamicFilePath != "" {
logrus.Fatal("dynamicfile is not set in backend-mode but dynamicfilepath is set")
}

if errs := mapper.ValidateBackendMode(cfg.BackendMode); len(errs) > 0 {
return cfg, utilerrors.NewAggregate(errs)
}
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.16

require (
github.com/aws/aws-sdk-go v1.44.107
github.com/fsnotify/fsnotify v1.4.9
github.com/gofrs/flock v0.7.0
github.com/manifoldco/promptui v0.9.0
github.com/onsi/ginkgo v1.14.0
Expand Down
6 changes: 4 additions & 2 deletions pkg/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ type IdentityMapping struct {
// list of Kubernetes groups. The username and groups are specified as templates
// that may optionally contain two template parameters:
//
// 1) "{{AccountID}}" is the 12 digit AWS ID.
// 2) "{{SessionName}}" is the role session name.
// 1. "{{AccountID}}" is the 12 digit AWS ID.
// 2. "{{SessionName}}" is the role session name.
//
// The meaning of SessionName depends on the type of entity assuming the role.
// In the case of an EC2 instance role this will be the EC2 instance ID. In the
Expand Down Expand Up @@ -169,4 +169,6 @@ type Config struct {
// understand we don't need to change
EC2DescribeInstancesQps int
EC2DescribeInstancesBurst int
//Dynamic File Path for DynamicFile BackendMode
DynamicFilePath string
}
244 changes: 244 additions & 0 deletions pkg/mapper/dynamicfile/dynamicfile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
package dynamicfile

import (
"encoding/json"
"errors"
"fmt"
"os"
"sync"
"time"

"github.com/fsnotify/fsnotify"
"github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/util/wait"
"sigs.k8s.io/aws-iam-authenticator/pkg/config"
)

type DynamicFileMapStore struct {
mutex sync.RWMutex
users map[string]config.UserMapping
roles map[string]config.RoleMapping
// Used as set.
awsAccounts map[string]interface{}
filename string
}

type DynamicFileData struct {
// RoleMappings is a list of mappings from AWS IAM Role to
// Kubernetes username + groups.
RoleMappings []config.RoleMapping `json:"mapRoles"`

// UserMappings is a list of mappings from AWS IAM User to
// Kubernetes username + groups.
UserMappings []config.UserMapping `json:"mapUsers"`
// AutoMappedAWSAccounts is a list of AWS accounts that are allowed without an explicit user/role mapping.
// IAM ARN from these accounts automatically maps to the Kubernetes username.
AutoMappedAWSAccounts []string `json:"mapAccounts"`
}

type ErrParsingMap struct {
errors []error
}

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.filename)
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(filename string) (*DynamicFileMapStore, error) {
ms := DynamicFileMapStore{}
ms.filename = filename
return &ms, nil
}

func (m *DynamicFileMapStore) startLoadDynamicFile(stopCh <-chan struct{}) {
go wait.Until(func() {
m.loadDynamicFile()
// start to watch the file change
watcher, err := fsnotify.NewWatcher()
if err != nil {
logrus.Errorf("startLoadDynamicFile: failed when call fsnotify.NewWatcher, %+v", err)
}
err = watcher.Add(m.filename)
if err != nil {
logrus.Errorf("startLoadDynamicFile: could not add file to watcher %v", err)
}

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)
}
}
}, time.Second, stopCh)
}

func ParseMap(filename string) (userMappings []config.UserMapping, roleMappings []config.RoleMapping, awsAccounts []string, err error) {
errs := make([]error, 0)
userMappings = make([]config.UserMapping, 0)
roleMappings = make([]config.RoleMapping, 0)

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 {
err = userMapping.Validate()
if err != nil {
errs = append(errs, err)
} else {
userMappings = append(userMappings, userMapping)
}
}

for _, roleMapping := range dynamicFileData.RoleMappings {
err = roleMapping.Validate()
if err != nil {
errs = append(errs, err)
} 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,
awsAccounts []string) {

ms.mutex.Lock()
defer ms.mutex.Unlock()
ms.users = make(map[string]config.UserMapping)
ms.roles = make(map[string]config.RoleMapping)
ms.awsAccounts = make(map[string]interface{})

for _, user := range userMappings {
ms.users[user.Key()] = user
}
for _, role := range roleMappings {
ms.roles[role.Key()] = role
}
for _, awsAccount := range awsAccounts {
ms.awsAccounts[awsAccount] = nil
}
}

// UserNotFound is the error returned when the user is not found in the config map.
var UserNotFound = errors.New("User not found in dynamic file")

// RoleNotFound is the error returned when the role is not found in the config map.
var RoleNotFound = errors.New("Role not found in dynamic file")

func (ms *DynamicFileMapStore) UserMapping(arn string) (config.UserMapping, error) {
ms.mutex.RLock()
defer ms.mutex.RUnlock()
for _, user := range ms.users {
if user.Matches(arn) {
return user, nil
}
}
return config.UserMapping{}, UserNotFound
}

func (ms *DynamicFileMapStore) RoleMapping(arn string) (config.RoleMapping, error) {
ms.mutex.RLock()
defer ms.mutex.RUnlock()
for _, role := range ms.roles {
if role.Matches(arn) {
return role, nil
}
}
return config.RoleMapping{}, RoleNotFound
}

func (ms *DynamicFileMapStore) AWSAccount(id string) bool {
ms.mutex.RLock()
defer ms.mutex.RUnlock()
_, ok := ms.awsAccounts[id]
return ok
}

func (ms *DynamicFileMapStore) LogMapping() {
ms.mutex.RLock()
defer ms.mutex.RUnlock()
for _, user := range ms.users {
logrus.Info(user)
}
for _, role := range ms.roles {
logrus.Info(role)
}
for awsAccount, _ := range ms.awsAccounts {
logrus.Info(awsAccount)
}
}
Loading

0 comments on commit ce51f94

Please sign in to comment.