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 }