github.com/wostzone/hub/auth@v0.0.0-20220118060317-7bb375743b17/pkg/aclstore/AclFileStore.go (about)

     1  package aclstore
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"os"
     7  	"path"
     8  	"sync"
     9  
    10  	"github.com/fsnotify/fsnotify"
    11  	"github.com/sirupsen/logrus"
    12  	"github.com/wostzone/hub/auth/pkg/authorize"
    13  	"github.com/wostzone/hub/lib/serve/pkg/watcher"
    14  	"gopkg.in/yaml.v3"
    15  )
    16  
    17  // DefaultAclFile recommended ACL filename for Hub authentication
    18  const DefaultAclFile = "hub.acl"
    19  
    20  // AclGroup is a map of group clients and roles
    21  type AclGroup map[string]string // map of clientID:role
    22  
    23  // AclFileStore stores ACL list in file.
    24  // It includes a file watcher to automatically reload on update.
    25  type AclFileStore struct {
    26  	clientID     string              // for logging
    27  	Groups       map[string]AclGroup `yaml:"groups"` // store by group ID
    28  	storePath    string
    29  	watcher      *fsnotify.Watcher
    30  	mutex        sync.RWMutex
    31  	clientGroups map[string]map[string]string // list of groups the client is a member of. Updated on load.
    32  }
    33  
    34  // Close the store
    35  func (aclStore *AclFileStore) Close() {
    36  	logrus.Infof("AclFileStore.Close: clientID='%s'", aclStore.clientID)
    37  	aclStore.mutex.Lock()
    38  	defer aclStore.mutex.Unlock()
    39  	if aclStore.watcher != nil {
    40  		aclStore.watcher.Close()
    41  		aclStore.watcher = nil
    42  	}
    43  }
    44  
    45  // GetGroups returns a list of groups a thing or user is a member of
    46  func (aclStore *AclFileStore) GetGroups(clientID string) []string {
    47  	groupsMemberOf := []string{}
    48  
    49  	aclStore.mutex.RLock()
    50  	defer aclStore.mutex.RUnlock()
    51  	cg := aclStore.clientGroups[clientID]
    52  	for groupName := range cg {
    53  		groupsMemberOf = append(groupsMemberOf, groupName)
    54  	}
    55  	return groupsMemberOf
    56  }
    57  
    58  // GetRole returns the highest role of a user has in a list of group
    59  // Intended to get client permissions in case of overlapping groups
    60  func (aclStore *AclFileStore) GetRole(clientID string, groupIDs []string) string {
    61  	highestRole := authorize.GroupRoleNone
    62  
    63  	aclStore.mutex.RLock()
    64  	defer aclStore.mutex.RUnlock()
    65  
    66  	cg := aclStore.clientGroups[clientID]
    67  
    68  	for _, groupID := range groupIDs {
    69  		role := cg[groupID]
    70  		if IsRoleGreaterEqual(role, highestRole) {
    71  			highestRole = role
    72  		}
    73  	}
    74  	logrus.Debugf("AclFileStore.GetRole: clientID=%s, highestRole=%s", clientID, highestRole)
    75  	return highestRole
    76  }
    77  
    78  // IsRoleGreaterEqual returns true if a user role has same or greater permissions
    79  // than the minimum role.
    80  func IsRoleGreaterEqual(role string, minRole string) bool {
    81  	if minRole == authorize.GroupRoleNone || role == minRole {
    82  		return true
    83  	}
    84  	if minRole == authorize.GroupRoleViewer && role != authorize.GroupRoleNone {
    85  		return true
    86  	}
    87  	if minRole == authorize.GroupRoleEditor && (role == authorize.GroupRoleManager) {
    88  		return true
    89  	}
    90  	return false
    91  }
    92  
    93  // Open the store
    94  // This reads the acl file and subscribes to file changes.
    95  // The ACL file MUST exist, even if it is empty.
    96  func (aclStore *AclFileStore) Open() error {
    97  	logrus.Infof("AclFileStore.Open: clientID='%s'", aclStore.clientID)
    98  
    99  	err := aclStore.Reload()
   100  	if err != nil {
   101  		return err
   102  	}
   103  	// watcher handles debounce of too many events
   104  	aclStore.watcher, err = watcher.WatchFile(aclStore.storePath, aclStore.Reload, aclStore.clientID)
   105  	return err
   106  }
   107  
   108  // Reload the ACL store from file
   109  func (aclStore *AclFileStore) Reload() error {
   110  	logrus.Infof("AclFileStore.Reload: clientID='%s'. Reloading acls from %s", aclStore.clientID, aclStore.storePath)
   111  
   112  	aclStore.mutex.Lock()
   113  	defer aclStore.mutex.Unlock()
   114  	raw, err := os.ReadFile(aclStore.storePath)
   115  	if err != nil {
   116  		logrus.Errorf("AclFileStore.Reload clientID='%s'. File '%s': Error opening the ACL file: %s", aclStore.clientID, aclStore.storePath, err)
   117  		return err
   118  	}
   119  	err = yaml.Unmarshal(raw, &aclStore.Groups)
   120  	if err != nil {
   121  		logrus.Errorf("AclFileStore.Reload clientID='%s'. File '%s': Error parsing the ACL file: %s", aclStore.clientID, aclStore.storePath, err)
   122  		return err
   123  	}
   124  
   125  	// update index to lookup the role of a client in a group:
   126  	// eg: [clientID] = {group:role, group:role, ...}
   127  	clientGroups := make(map[string]map[string]string)
   128  	for groupName, group := range aclStore.Groups {
   129  		for client, role := range group {
   130  			cg := clientGroups[client]
   131  			if cg == nil {
   132  				cg = make(map[string]string)
   133  				clientGroups[client] = cg
   134  			}
   135  			cg[groupName] = role
   136  		}
   137  	}
   138  	logrus.Infof("AclFileStore.Reload clientID='%s'. Reloaded %d groups", aclStore.clientID, len(clientGroups))
   139  	aclStore.clientGroups = clientGroups
   140  	return nil
   141  }
   142  
   143  // SetRole sets a user ACL and update the store.
   144  // This updates the user's role, saves it to a temp file and move the result to the store file.
   145  // Interruptions will not lead to data corruption as the resulting acl file is only moved after successful write.
   146  // Note that concurrent writes by different processes is not supported and can lead to one of the
   147  // writes being ignored.
   148  //  clientID login name to assign the role
   149  //  groupID  group where the role applies
   150  //  role     one of GroupRoleViewer, GroupRoleEditor, GroupRoleManager, GroupRoleThing or GroupRoleNone to remove the role
   151  func (aclStore *AclFileStore) SetRole(clientID string, groupID string, role string) error {
   152  	// Prevent concurrently running Reload and SetRole
   153  	aclStore.mutex.Lock()
   154  	defer aclStore.mutex.Unlock()
   155  
   156  	aclGroup := aclStore.Groups[groupID]
   157  	if aclGroup == nil {
   158  		aclGroup = AclGroup{}
   159  		aclStore.Groups[groupID] = aclGroup
   160  	}
   161  	if role == authorize.GroupRoleNone {
   162  		delete(aclGroup, clientID)
   163  	} else {
   164  		aclGroup[clientID] = role
   165  	}
   166  	folder := path.Dir(aclStore.storePath)
   167  	tempFileName, err := WriteAclsToTempFile(folder, aclStore.Groups)
   168  	if err != nil {
   169  		logrus.Errorf("AclFileStore.SetRole clientID='%s': %s", aclStore.clientID, err)
   170  		return err
   171  	}
   172  
   173  	err = os.Rename(tempFileName, aclStore.storePath)
   174  	if err != nil {
   175  		logrus.Errorf("AclFileStore.SetRole. clientID='%s'. Error renaming temp file to store: %s", aclStore.clientID, err)
   176  		return err
   177  	}
   178  
   179  	logrus.Infof("AclFileStore.SetRole: clientID='%s' set a role '%s' in group '%s'", clientID, role, groupID)
   180  	return err
   181  }
   182  
   183  // WriteAclsToTempFile write the ACL store to a temp file
   184  func WriteAclsToTempFile(folder string, acls map[string]AclGroup) (tempFileName string, err error) {
   185  	file, err := os.CreateTemp(folder, "hub-aclfilestore")
   186  	// file, err := os.OpenFile(path, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0600)
   187  	if err != nil {
   188  		err := fmt.Errorf("WriteAclsToTempFile: Failed open temp acl file: %s", err)
   189  		return "", err
   190  	}
   191  	tempFileName = file.Name()
   192  	logrus.Infof("WriteAclsToTempFile tempfile: %s", tempFileName)
   193  	defer file.Close()
   194  
   195  	data, _ := yaml.Marshal(acls)
   196  	writer := bufio.NewWriter(file)
   197  	_, err = writer.Write(data)
   198  	if err != nil {
   199  		err := fmt.Errorf("WriteAclsToTempFile: Failed writing temp acl file: %s", err)
   200  		return tempFileName, err
   201  	}
   202  
   203  	writer.Flush()
   204  	// err := ioutil.WriteFile(path, data, perm)
   205  	return tempFileName, err
   206  }
   207  
   208  // NewAclFileStore creates an instance of a file based ACL store
   209  //  filepath is the location of the store. See also DefaultAclFilename for the recommended name.
   210  //  clientID is for logging which authservice is accessing it
   211  func NewAclFileStore(filepath string, clientID string) *AclFileStore {
   212  	store := &AclFileStore{
   213  		clientID:     clientID,
   214  		Groups:       make(map[string]AclGroup),
   215  		clientGroups: make(map[string]map[string]string), // list of groups the client is a member of. Updated on load.
   216  
   217  		storePath: filepath,
   218  	}
   219  	return store
   220  }