├── .travis.yml ├── README.md ├── acl_qualifier_go1.7.go ├── LICENSE-MIT ├── acl_not_impl.go ├── acl_linux.go ├── LICENSE-APACHE ├── acl_test.go └── acl.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | os: 4 | - linux 5 | 6 | go: 7 | - "1.9" 8 | - "1.10" 9 | - "1.11" 10 | - "1.12" 11 | - "1.13" 12 | - "1.14" 13 | - 1.x 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | # go-acl [![GoDoc](https://godoc.org/github.com/joshlf/go-acl?status.svg)](https://godoc.org/github.com/joshlf/go-acl) 10 | Go support for Access Control Lists 11 | 12 | --- 13 | #### Minimum Requirements 14 | - minimum supported compiler version is Go 1.9 15 | -------------------------------------------------------------------------------- /acl_qualifier_go1.7.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the LICENSE-APACHE file) or 4 | // the MIT license (the LICENSE-MIT file) at your option. This file may not be 5 | // copied, modified, or distributed except according to those terms. 6 | 7 | // +build go1.9 8 | 9 | package acl 10 | 11 | import "os/user" 12 | 13 | func init() { 14 | formatQualifier = func(q string, tag Tag) string { 15 | switch tag { 16 | case TagUser: 17 | usr, err := user.LookupId(q) 18 | if err != nil { 19 | return q 20 | } 21 | return usr.Username 22 | case TagGroup: 23 | grp, err := user.LookupGroupId(q) 24 | if err != nil { 25 | return q 26 | } 27 | return grp.Name 28 | default: 29 | return q 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /acl_not_impl.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the LICENSE-APACHE file) or 4 | // the MIT license (the LICENSE-MIT file) at your option. This file may not be 5 | // copied, modified, or distributed except according to those terms. 6 | 7 | // +build !linux 8 | 9 | package acl 10 | 11 | import ( 12 | "os" 13 | "syscall" 14 | ) 15 | 16 | type tag int 17 | 18 | const ( 19 | // While these aren't actually meaningful, 20 | // we still want them to be distinct so they 21 | // don't compare as equal 22 | tagUndefined Tag = iota 23 | tagUserObj 24 | tagUser 25 | tagGroupObj 26 | tagGroup 27 | tagMask 28 | tagOther 29 | ) 30 | 31 | func get(path string) (ACL, error) { 32 | return nil, syscall.ENOTSUP 33 | } 34 | 35 | func fget(f *os.File) (ACL, error) { 36 | return nil, syscall.ENOTSUP 37 | } 38 | 39 | func getDefault(path string) (ACL, error) { 40 | return nil, syscall.ENOTSUP 41 | } 42 | 43 | func fgetDefault(f *os.File) (ACL, error) { 44 | return nil, syscall.ENOTSUP 45 | } 46 | 47 | func set(path string, acl ACL) error { 48 | return syscall.ENOTSUP 49 | } 50 | 51 | func fset(f *os.File, acl ACL) error { 52 | return syscall.ENOTSUP 53 | } 54 | 55 | func setDefault(path string, acl ACL) error { 56 | return syscall.ENOTSUP 57 | } 58 | 59 | func fsetDefault(f *os.File, acl ACL) error { 60 | return syscall.ENOTSUP 61 | } 62 | -------------------------------------------------------------------------------- /acl_linux.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the LICENSE-APACHE file) or 4 | // the MIT license (the LICENSE-MIT file) at your option. This file may not be 5 | // copied, modified, or distributed except according to those terms. 6 | 7 | package acl 8 | 9 | import ( 10 | "encoding/binary" 11 | "fmt" 12 | "math" 13 | "os" 14 | "sort" 15 | "strconv" 16 | "sync" 17 | "syscall" 18 | 19 | "golang.org/x/sys/unix" 20 | ) 21 | 22 | /* 23 | NOTE: This implementation is largely based on Linux's libacl. 24 | */ 25 | 26 | type tag int 27 | 28 | const ( 29 | // defined in sys/acl.h 30 | tagUndefined Tag = 0x00 31 | tagUserObj = 0x01 32 | tagUser = 0x02 33 | tagGroupObj = 0x04 34 | tagGroup = 0x08 35 | tagMask = 0x10 36 | tagOther = 0x20 37 | 38 | // defined in include/acl_ea.h (see libacl source) 39 | aclEAAccess = "system.posix_acl_access" 40 | aclEADefault = "system.posix_acl_default" 41 | aclEAVersion = 2 42 | aclEAEntrySize = 8 43 | aclUndefinedID = math.MaxUint32 // defined in sys/acl.h 44 | ) 45 | 46 | const defaultbuflen = 64 47 | 48 | type fileObj interface { 49 | Getxattr(attr string, dest []byte) (int, error) 50 | Setxattr(attr string, dest []byte, flags int) error 51 | Stat() (os.FileInfo, error) 52 | } 53 | 54 | type path string 55 | 56 | func (p path) Getxattr(attr string, dest []byte) (int, error) { 57 | return syscall.Getxattr(string(p), attr, dest) 58 | } 59 | 60 | func (p path) Setxattr(attr string, dest []byte, flags int) error { 61 | return syscall.Setxattr(string(p), attr, dest, flags) 62 | } 63 | 64 | func (p path) Stat() (os.FileInfo, error) { 65 | return os.Stat(string(p)) 66 | } 67 | 68 | type file struct { 69 | *os.File 70 | } 71 | 72 | func (f file) Getxattr(attr string, dest []byte) (int, error) { 73 | return unix.Fgetxattr(int(f.Fd()), attr, dest) 74 | } 75 | 76 | func (f file) Setxattr(attr string, dest []byte, flags int) error { 77 | return unix.Fsetxattr(int(f.Fd()), attr, dest, flags) 78 | } 79 | 80 | func (f file) Stat() (os.FileInfo, error) { 81 | return f.File.Stat() 82 | } 83 | 84 | func get(p string) (ACL, error) { 85 | return getType(path(p), aclEAAccess) 86 | } 87 | 88 | func fget(f *os.File) (ACL, error) { 89 | return getType(file{f}, aclEAAccess) 90 | } 91 | 92 | func getDefault(p string) (ACL, error) { 93 | return getType(path(p), aclEADefault) 94 | } 95 | 96 | func fgetDefault(f *os.File) (ACL, error) { 97 | return getType(file{f}, aclEADefault) 98 | } 99 | 100 | func set(p string, acl ACL) error { 101 | return setType(path(p), aclEAAccess, acl) 102 | } 103 | 104 | func fset(f *os.File, acl ACL) error { 105 | return setType(file{f}, aclEAAccess, acl) 106 | } 107 | 108 | func setDefault(p string, acl ACL) error { 109 | return setType(path(p), aclEADefault, acl) 110 | } 111 | 112 | func fsetDefault(f *os.File, acl ACL) error { 113 | return setType(file{f}, aclEADefault, acl) 114 | } 115 | 116 | func xattrFromACL(acl ACL) (xattr []byte, err error) { 117 | // NOTE(joshlf): I honestly don't know why sorting is required - 118 | // all I know is that when the entries are left unsorted, the 119 | // setxattrs syscall sometimes returns EINVAL, but when they're 120 | // sorted, it never does. I can't find either documentation or 121 | // kernel code to explain this behavior. The only evidence is 122 | // the source code for libacl's acl_check, which checks the order. 123 | acl = append(ACL(nil), acl...) 124 | sort.Sort(sortableACL(acl)) 125 | 126 | xattr = make([]byte, 4+8*len(acl)) 127 | binary.LittleEndian.PutUint32(xattr, aclEAVersion) 128 | xattrtmp := xattr[4:] 129 | for _, ent := range acl { 130 | binary.LittleEndian.PutUint16(xattrtmp, uint16(ent.Tag)) 131 | binary.LittleEndian.PutUint16(xattrtmp[2:], uint16(ent.Perms)) 132 | if ent.Tag == TagUser || ent.Tag == TagGroup { 133 | qid, err := strconv.ParseUint(ent.Qualifier, 10, 32) 134 | if err != nil { 135 | return nil, fmt.Errorf("parse qualifier: %v", err) 136 | } 137 | binary.LittleEndian.PutUint32(xattrtmp[4:], uint32(qid)) 138 | } else { 139 | binary.LittleEndian.PutUint32(xattrtmp[4:], aclUndefinedID) 140 | } 141 | xattrtmp = xattrtmp[8:] 142 | } 143 | return xattr, nil 144 | } 145 | 146 | // sort according to the same order as required by libacl's acl_check 147 | 148 | func entryPriority(e Entry) int { 149 | switch e.Tag { 150 | case TagUserObj: 151 | return 0 152 | case TagUser: 153 | return 1 154 | case TagGroupObj: 155 | return 2 156 | case TagGroup: 157 | return 3 158 | case TagMask: 159 | return 4 160 | default: 161 | return 5 162 | } 163 | } 164 | 165 | type sortableACL ACL 166 | 167 | func (s sortableACL) Len() int { return len(s) } 168 | func (s sortableACL) Less(i, j int) bool { 169 | return entryPriority(s[i]) < entryPriority(s[j]) 170 | } 171 | func (s sortableACL) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 172 | 173 | func aclFromXattr(xattr []byte) (acl ACL, err error) { 174 | if len(xattr) < 4 { 175 | return nil, syscall.EINVAL 176 | } 177 | version := binary.LittleEndian.Uint32(xattr) 178 | xattr = xattr[4:] 179 | if version != aclEAVersion { 180 | return nil, syscall.EINVAL 181 | } 182 | if len(xattr)%aclEAEntrySize != 0 { 183 | return nil, syscall.EINVAL 184 | } 185 | 186 | for len(xattr) > 0 { 187 | etag := binary.LittleEndian.Uint16(xattr) 188 | sperm := binary.LittleEndian.Uint16(xattr[2:]) 189 | qid := binary.LittleEndian.Uint32(xattr[4:]) 190 | 191 | ent := Entry{ 192 | Tag: Tag(etag), 193 | Perms: os.FileMode(sperm), 194 | } 195 | if ent.Tag == TagUser || ent.Tag == TagGroup { 196 | ent.Qualifier = fmt.Sprint(qid) 197 | } 198 | 199 | acl = append(acl, ent) 200 | xattr = xattr[8:] 201 | } 202 | 203 | return acl, nil 204 | } 205 | 206 | // based on libacl's acl_get_file 207 | func getType(f fileObj, attr string) (ACL, error) { 208 | buf := bufpool.Get().([]byte) 209 | defer func() { bufpool.Put(buf) }() 210 | 211 | sz, err := f.Getxattr(attr, buf) 212 | if sz == -1 && err == syscall.ERANGE { 213 | sz, err = f.Getxattr(attr, nil) 214 | if sz <= 0 { 215 | return nil, err 216 | } 217 | buf = make([]byte, sz) 218 | sz, err = f.Getxattr(attr, buf) 219 | } 220 | 221 | switch { 222 | case sz > 0: 223 | return aclFromXattr(buf[:sz]) 224 | case err == syscall.ENODATA: 225 | // TODO(joshlf): acl_get_file also checks for ENOATTR, 226 | // but it's not defined in syscall? 227 | fi, err := f.Stat() 228 | if err != nil { 229 | return nil, err 230 | } 231 | if attr == aclEADefault { 232 | if fi.IsDir() { 233 | return nil, nil 234 | } 235 | return nil, syscall.EACCES 236 | } else { 237 | return FromUnix(fi.Mode()), nil 238 | } 239 | default: 240 | return nil, err 241 | } 242 | } 243 | 244 | // based on libacl's acl_set_file 245 | func setType(f fileObj, attr string, acl ACL) error { 246 | if attr == aclEADefault { 247 | fi, err := f.Stat() 248 | if err != nil { 249 | return err 250 | } 251 | 252 | // non-directories can't have default ACLs 253 | if !fi.IsDir() { 254 | return syscall.EACCES 255 | } 256 | } 257 | 258 | xattr, err := xattrFromACL(acl) 259 | if err != nil { 260 | return err 261 | } 262 | return f.Setxattr(attr, xattr, 0) 263 | } 264 | 265 | var bufpool = sync.Pool{ 266 | New: func() interface{} { return make([]byte, defaultbuflen) }, 267 | } 268 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /acl_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the LICENSE-APACHE file) or 4 | // the MIT license (the LICENSE-MIT file) at your option. This file may not be 5 | // copied, modified, or distributed except according to those terms. 6 | 7 | package acl 8 | 9 | import ( 10 | "fmt" 11 | "math/rand" 12 | "os" 13 | "path/filepath" 14 | "reflect" 15 | "testing" 16 | 17 | "github.com/joshlf/testutil" 18 | ) 19 | 20 | func TestGet(t *testing.T) { 21 | f := testutil.MustTempFile(t, "", "acl").Name() 22 | defer os.Remove(f) 23 | _, err := Get(f) 24 | testutil.Must(t, err) 25 | 26 | d := testutil.MustTempDir(t, "", "acl") 27 | defer os.Remove(d) 28 | _, err = GetDefault(d) 29 | testutil.Must(t, err) 30 | } 31 | 32 | func TestFGet(t *testing.T) { 33 | f := testutil.MustTempFile(t, "", "acl") 34 | defer func() { 35 | _ = f.Close() 36 | _ = os.Remove(f.Name()) 37 | }() 38 | _, err := FGet(f) 39 | testutil.Must(t, err) 40 | 41 | d := testutil.MustTempDir(t, "", "acl") 42 | defer func() { 43 | _ = os.Remove(d) 44 | }() 45 | dir, err := os.Open(d) 46 | defer func() { 47 | _ = dir.Close() 48 | }() 49 | testutil.Must(t, err) 50 | _, err = FGetDefault(dir) 51 | testutil.Must(t, err) 52 | } 53 | 54 | func TestSet(t *testing.T) { 55 | /* 56 | Access ACL 57 | */ 58 | f := testutil.MustTempFile(t, "", "acl").Name() 59 | defer os.Remove(f) 60 | acl, err := Get(f) 61 | testutil.Must(t, err) 62 | for i := range acl { 63 | acl[i].Perms = (^acl[i].Perms) & 0x7 // Flip the rwx bits 64 | } 65 | err = Set(f, acl) 66 | testutil.Must(t, err) 67 | acl2, err := Get(f) 68 | testutil.Must(t, err) 69 | if !reflect.DeepEqual(acl, acl2) { 70 | t.Errorf("unexpected ACL: want %v; got %v", acl, acl2) 71 | } 72 | 73 | /* 74 | Default ACL 75 | */ 76 | d := testutil.MustTempDir(t, "", "acl") 77 | defer os.Remove(d) 78 | // reuse the acl from above since we know it's valid 79 | err = SetDefault(d, acl) 80 | testutil.Must(t, err) 81 | acl2, err = GetDefault(d) 82 | testutil.Must(t, err) 83 | if !reflect.DeepEqual(acl, acl2) { 84 | t.Errorf("unexpected default ACL: want %v; got %v", acl, acl2) 85 | } 86 | } 87 | 88 | func TestFSet(t *testing.T) { 89 | f := testutil.MustTempFile(t, "", "acl") 90 | defer func() { 91 | _ = f.Close() 92 | _ = os.Remove(f.Name()) 93 | }() 94 | acl, err := FGet(f) 95 | testutil.Must(t, err) 96 | for i := range acl { 97 | acl[i].Perms = (^acl[i].Perms) & 0x7 98 | } 99 | err = FSet(f, acl) 100 | testutil.Must(t, err) 101 | acl2, err := FGet(f) 102 | testutil.Must(t, err) 103 | if !reflect.DeepEqual(acl, acl2) { 104 | t.Errorf("unexpected ACL: want %v; got %v", acl, acl2) 105 | } 106 | 107 | d := testutil.MustTempDir(t, "", "acl") 108 | defer func() { 109 | _ = os.Remove(d) 110 | }() 111 | dir, err := os.Open(d) 112 | defer func() { 113 | _ = dir.Close() 114 | }() 115 | err = FSetDefault(dir, acl) 116 | testutil.Must(t, err) 117 | acl2, err = FGetDefault(dir) 118 | testutil.Must(t, err) 119 | if !reflect.DeepEqual(acl, acl2) { 120 | t.Errorf("unexpected default ACL: want %v; got %v", acl, acl2) 121 | } 122 | } 123 | 124 | var ( 125 | base = ACL{ 126 | {TagUserObj, "", 7}, 127 | {TagGroupObj, "", 0}, 128 | {TagOther, "", 0}, 129 | } 130 | 131 | addTestCases = []struct { 132 | Before ACL 133 | Add []Entry 134 | Afer ACL 135 | }{ 136 | // Make sure mask is generated 137 | { 138 | base, 139 | []Entry{{TagUser, "0", 4}, {TagGroup, "0", 2}}, 140 | append(ACL{{TagUser, "0", 4}, {TagGroup, "0", 2}, {TagMask, "", 6}}, base...), 141 | }, 142 | // Make sure mask is not generated if a mask is supplied 143 | { 144 | base, 145 | []Entry{{TagUser, "0", 4}, {TagGroup, "0", 2}, {TagMask, "", 1}}, 146 | append(ACL{{TagUser, "0", 4}, {TagGroup, "0", 2}, {TagMask, "", 1}}, base...), 147 | }, 148 | // Make sure the original mask is overridden 149 | { 150 | append(ACL{{TagMask, "", 7}}, base...), 151 | []Entry{{TagUser, "0", 4}, {TagGroup, "0", 2}}, 152 | append(ACL{{TagUser, "0", 4}, {TagGroup, "0", 2}, {TagMask, "", 6}}, base...), 153 | }, 154 | // Make sure TagUser, TagGroup, or TagGroupObj in original is used 155 | // in calculating new mask 156 | { 157 | append(ACL{{TagUser, "0", 4}, {TagMask, "", 0}}, base...), 158 | []Entry{{TagGroup, "0", 2}, {TagGroupObj, "", 1}}, 159 | ACL{{TagUser, "0", 4}, {TagGroup, "0", 2}, {TagMask, "", 7}, 160 | {TagUserObj, "", 7}, 161 | {TagGroupObj, "", 1}, 162 | {TagOther, "", 0}}, 163 | }, 164 | // Make sure TagUser or TagGroup in original is NOT used 165 | // in calculating new mask if it's overwritten 166 | { 167 | append(ACL{{TagUser, "0", 7}, {TagMask, "", 0}}, base...), 168 | []Entry{{TagUser, "0", 4}, {TagGroup, "0", 2}}, 169 | append(ACL{{TagUser, "0", 4}, {TagGroup, "0", 2}, {TagMask, "", 6}}, base...), 170 | }, 171 | } 172 | ) 173 | 174 | func TestAdd(t *testing.T) { 175 | f := testutil.MustTempFile(t, "", "acl").Name() 176 | defer os.Remove(f) 177 | 178 | for i, c := range addTestCases { 179 | err := Set(f, c.Before) 180 | testutil.Must(t, err) 181 | err = Add(f, c.Add...) 182 | testutil.Must(t, err) 183 | acl, err := Get(f) 184 | testutil.Must(t, err) 185 | 186 | m1 := make(map[Entry]bool) 187 | m2 := make(map[Entry]bool) 188 | for _, e := range acl { 189 | m1[e] = true 190 | } 191 | for _, e := range c.Afer { 192 | m2[e] = true 193 | } 194 | 195 | if !reflect.DeepEqual(m1, m2) { 196 | t.Errorf("case %v: unexpected ACL: want %v; got %v", i, c.Afer, acl) 197 | } 198 | } 199 | } 200 | 201 | func TestFAdd(t *testing.T) { 202 | f := testutil.MustTempFile(t, "", "acl") 203 | defer func() { 204 | _ = f.Close() 205 | _ = os.Remove(f.Name()) 206 | }() 207 | 208 | for i, c := range addTestCases { 209 | err := FSet(f, c.Before) 210 | testutil.Must(t, err) 211 | err = FAdd(f, c.Add...) 212 | testutil.Must(t, err) 213 | acl, err := FGet(f) 214 | testutil.Must(t, err) 215 | 216 | m1 := make(map[Entry]bool) 217 | m2 := make(map[Entry]bool) 218 | for _, e := range acl { 219 | m1[e] = true 220 | } 221 | for _, e := range c.Afer { 222 | m2[e] = true 223 | } 224 | 225 | if !reflect.DeepEqual(m1, m2) { 226 | t.Errorf("case %v: unexpected ACL: want %v; got %v", i, c.Afer, acl) 227 | } 228 | } 229 | } 230 | 231 | func TestDefault(t *testing.T) { 232 | d := testutil.MustTempDir(t, "", "acl") 233 | defer os.RemoveAll(d) 234 | // Set default ACL to no permissions, which is pretty much 235 | // guaranteed not to be the system-wide default. 236 | // That way we know it's not a fluke if that's the ACL 237 | // on a newly-created file. 238 | dacl := ACL{Entry{Tag: TagUserObj}, Entry{Tag: TagUser, Qualifier: "0"}, 239 | Entry{Tag: TagGroupObj}, Entry{Tag: TagGroup, Qualifier: "0"}, 240 | Entry{Tag: TagMask}, Entry{Tag: TagOther}} 241 | err := SetDefault(d, dacl) 242 | testutil.Must(t, err) 243 | 244 | _, err = os.Create(filepath.Join(d, "file")) 245 | testutil.Must(t, err) 246 | acl, err := Get(filepath.Join(d, "file")) 247 | testutil.Must(t, err) 248 | if !reflect.DeepEqual(dacl, acl) { 249 | t.Errorf("access ACL does not match parent's default ACL: got %v; want %v", 250 | acl, dacl) 251 | } 252 | 253 | err = os.Mkdir(filepath.Join(d, "dir"), 0666) 254 | testutil.Must(t, err) 255 | dacl2, err := GetDefault(filepath.Join(d, "dir")) 256 | testutil.Must(t, err) 257 | if !reflect.DeepEqual(dacl, dacl2) { 258 | t.Errorf("default ACL does not match parent's default ACL: got %v; want %v", 259 | dacl2, dacl) 260 | } 261 | } 262 | 263 | var ( 264 | userObj = Entry{Tag: TagUserObj} 265 | groupObj = Entry{Tag: TagGroupObj} 266 | other = Entry{Tag: TagOther} 267 | usr = Entry{Tag: TagUser} // Don't collide with os/user (imported by other files) 268 | group = Entry{Tag: TagGroup} 269 | mask = Entry{Tag: TagMask} 270 | ) 271 | 272 | var validACLs = []ACL{ 273 | {userObj, groupObj, other}, 274 | {userObj, groupObj, other, usr, mask}, 275 | {userObj, groupObj, other, group, mask}, 276 | {userObj, groupObj, other, usr, group, mask}, 277 | {userObj, groupObj, other, usr, {Tag: TagUser, Qualifier: " "}, group, {Tag: TagGroup, Qualifier: " "}, mask}, 278 | } 279 | 280 | var invalidACLs = []ACL{ 281 | {}, // No user, group, or other entries 282 | {userObj}, // No group or other entries 283 | {groupObj}, // No user or other entries 284 | {other}, // No user or group entries 285 | {userObj, groupObj}, // No other entry 286 | {userObj, other}, // No group entry 287 | {groupObj, other}, // No user entry 288 | 289 | {userObj, groupObj, other, {}}, // Invalid tag type 290 | {userObj, groupObj, other, usr}, // No mask 291 | {userObj, groupObj, other, group}, // No mask 292 | {userObj, groupObj, other, usr, usr, mask}, // Duplicate user qualifiers 293 | {userObj, groupObj, other, group, group, mask}, // Duplicate group qualifiers 294 | {userObj, groupObj, other, usr, mask, mask}, // Duplicate mask entries 295 | } 296 | 297 | func TestIsValid(t *testing.T) { 298 | f := testutil.MustTempFile(t, "", "acl").Name() 299 | defer os.Remove(f) 300 | acl, err := Get(f) 301 | testutil.Must(t, err) 302 | if !acl.IsValid() { 303 | t.Errorf("ACL reported invalid: %v", acl) 304 | } 305 | for _, v := range validACLs { 306 | if !v.IsValid() { 307 | t.Errorf("ACL reported invalid: %v", v) 308 | } 309 | } 310 | for _, i := range invalidACLs { 311 | if i.IsValid() { 312 | t.Errorf("ACL reported valid: %v", i) 313 | } 314 | } 315 | } 316 | 317 | func TestUnix(t *testing.T) { 318 | rand.Seed(1676218289) 319 | 320 | aclFromUnix := func(user, group, other os.FileMode) ACL { 321 | return ACL{ 322 | {Tag: TagUserObj, Perms: user}, 323 | {Tag: TagGroupObj, Perms: group}, 324 | {Tag: TagOther, Perms: other}, 325 | } 326 | } 327 | // Run through every possible unix permissions bitmask 328 | // and make sure it is generated with ToUnix and its 329 | // accompanying ACL is generated with FromUnix. 330 | for user := os.FileMode(0); user < 8; user++ { 331 | for group := os.FileMode(0); group < 8; group++ { 332 | for other := os.FileMode(0); other < 8; other++ { 333 | perms := (user << 6) | (group << 3) | other 334 | acl := aclFromUnix(user, group, other) 335 | permstmp := ToUnix(acl) 336 | // Add in extra high bits to make sure 337 | // we're only considering permissions bits 338 | max := ^os.FileMode(0) 339 | acltmp := FromUnix(perms | max<<9) 340 | if permstmp != perms { 341 | t.Errorf("unexpected perms: want %v; got %v; acl: %v", perms, permstmp, acl) 342 | } 343 | if !reflect.DeepEqual(acltmp, acl) { 344 | t.Errorf("unexpected acl: want %v; got %v; perms: %v", acl, acltmp, perms) 345 | } 346 | } 347 | } 348 | } 349 | 350 | // Test to make sure that ToUnix can handle ACLs where 351 | // the entries are in any order and contain other entries 352 | // (besides those of tag UserObj, GroupObj, and Other) 353 | 354 | // Now, aclFromUnix will randomly generate an ACL with 355 | // the entries for UserObj, GroupObj, and Other having 356 | // the given permissions, but in a random order and with 357 | // entries of other tag types interspersed 358 | aclFromUnix = func(user, group, other os.FileMode) ACL { 359 | var otherTagTypes = []Tag{TagUser, TagGroup, TagMask} 360 | extraEntries := int(rand.ExpFloat64()) 361 | 362 | // First, make an ACL that starts with the entries 363 | // we want and then contains random other entries 364 | a := ACL{ 365 | {Tag: TagUserObj, Perms: user}, 366 | {Tag: TagGroupObj, Perms: group}, 367 | {Tag: TagOther, Perms: other}, 368 | } 369 | for i := 0; i < extraEntries; i++ { 370 | tag := otherTagTypes[rand.Int()%len(otherTagTypes)] 371 | a = append(a, Entry{Tag: tag, Perms: os.FileMode(rand.Uint32()) & 7}) 372 | } 373 | 374 | // Now permute them in a random order 375 | order := rand.Perm(len(a)) 376 | b := make(ACL, len(a)) 377 | for i := range b { 378 | b[i] = a[order[i]] 379 | } 380 | return b 381 | } 382 | 383 | const rounds = 100 384 | 385 | // Run through every possible unix permissions bitmask 386 | // and make sure it is generated with ToUnix and its 387 | // accompanying ACL is generated with FromUnix. 388 | for user := os.FileMode(0); user < 8; user++ { 389 | for group := os.FileMode(0); group < 8; group++ { 390 | for other := os.FileMode(0); other < 8; other++ { 391 | perms := (user << 6) | (group << 3) | other 392 | for i := 0; i < rounds; i++ { 393 | acl := aclFromUnix(user, group, other) 394 | permstmp := ToUnix(acl) 395 | if permstmp != perms { 396 | t.Errorf("unexpected perms: want %v; got %v; acl: %v", perms, permstmp, acl) 397 | } 398 | } 399 | } 400 | } 401 | } 402 | } 403 | 404 | func ExampleString() { 405 | acl := ACL{ 406 | {Tag: TagUserObj, Perms: 7}, 407 | {Tag: TagGroupObj, Perms: 6}, 408 | {Tag: TagOther, Perms: 5}, 409 | // It'd be nice to use different UIDs here, but root 410 | // is the only user whose UID is the same on all Unices 411 | {Tag: TagUser, Qualifier: "0", Perms: 4}, 412 | {Tag: TagUser, Qualifier: "0", Perms: 3}, 413 | {Tag: TagGroup, Qualifier: "0", Perms: 2}, 414 | {Tag: TagMask, Perms: 2}, 415 | } 416 | fmt.Println(acl) 417 | fmt.Println(acl.StringLong()) 418 | 419 | // Output: u::rwx,g::rw-,o::r-x,u:root:r--,u:root:-wx,g:root:-w-,m::-w- 420 | // user::rwx 421 | // group::rw- #effective:-w- 422 | // other::r-x 423 | // user:root:r-- #effective:--- 424 | // user:root:-wx #effective:-w- 425 | // group:root:-w- 426 | // mask::-w- 427 | } 428 | -------------------------------------------------------------------------------- /acl.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the LICENSE-APACHE file) or 4 | // the MIT license (the LICENSE-MIT file) at your option. This file may not be 5 | // copied, modified, or distributed except according to those terms. 6 | 7 | // Package acl implements POSIX.1e draft 17-compliant 8 | // manipulation of access control lists (ACLs). 9 | // See the acl manpage for details: http://linux.die.net/man/5/acl 10 | // 11 | // Currently, only Linux is supported. On systems which 12 | // are not supported, all calls will return the error 13 | // syscall.ENOTSUP. 14 | package acl 15 | 16 | import ( 17 | "errors" 18 | "fmt" 19 | "os" 20 | "strings" 21 | ) 22 | 23 | // ACL represents an access control list as defined 24 | // in the POSIX.1e draft standard. If an ACL is not 25 | // valid (see the IsValid method), the behavior of 26 | // the functions and methods of this package is 27 | // undefined. 28 | type ACL []Entry 29 | 30 | // FromUnix generates an ACL equivalent to the given 31 | // unix permissions bitmask. All non-permission bits 32 | // in perms are ignored. 33 | func FromUnix(perms os.FileMode) ACL { 34 | return ACL{ 35 | {Tag: TagUserObj, Perms: (perms >> 6) & 7}, 36 | {Tag: TagGroupObj, Perms: (perms >> 3) & 7}, 37 | {Tag: TagOther, Perms: perms & 7}, 38 | } 39 | } 40 | 41 | // ToUnix returns the unix permissions bitmask 42 | // encoded by a. If a is not valid as defined 43 | // by a.IsValid, the behavior of ToUnix is 44 | // undefined. 45 | func ToUnix(a ACL) os.FileMode { 46 | var perms os.FileMode 47 | for _, e := range a { 48 | switch e.Tag { 49 | case TagUserObj: 50 | perms |= (e.perms() << 6) 51 | case TagGroupObj: 52 | perms |= (e.perms() << 3) 53 | case TagOther: 54 | perms |= e.perms() 55 | } 56 | } 57 | return perms 58 | } 59 | 60 | // IsValid returns whether a is a valid ACL as defined 61 | // by the POSIX.1e draft standard. 62 | // 63 | // Specifically, a valid ACL must conform to the following 64 | // rules: 65 | // - it contains exactly one entry each with the tag TagUserObj, TagGroupObj, and TagOther 66 | // - it may contain zero or more entries with the tags TagUser or TagGroup 67 | // - if it contains any entries with the tag TagUser or TagGroup, it must contain exactly one 68 | // entry with the tag TagMask; otherwise, such an entry is optional (there can be zero or one) 69 | // - all qualifiers must be unique among entries of the same tag type (TagUser or TagGroup) 70 | func (a ACL) IsValid() bool { 71 | var numUserObj, numGroupObj, numOther int 72 | var numMask, numUserOrGroup int 73 | users := make(map[string]bool) 74 | groups := make(map[string]bool) 75 | for _, e := range a { 76 | switch e.Tag { 77 | case TagUserObj: 78 | numUserObj++ 79 | case TagGroupObj: 80 | numGroupObj++ 81 | case TagOther: 82 | numOther++ 83 | case TagMask: 84 | numMask++ 85 | case TagUser: 86 | numUserOrGroup++ 87 | if users[e.Qualifier] { 88 | return false 89 | } 90 | users[e.Qualifier] = true 91 | case TagGroup: 92 | numUserOrGroup++ 93 | if groups[e.Qualifier] { 94 | return false 95 | } 96 | groups[e.Qualifier] = true 97 | default: 98 | return false 99 | } 100 | } 101 | switch { 102 | case numUserObj != 1: 103 | return false 104 | case numGroupObj != 1: 105 | return false 106 | case numOther != 1: 107 | return false 108 | case numUserOrGroup > 0 && numMask == 0: 109 | return false 110 | case numMask > 1: 111 | return false 112 | } 113 | return true 114 | } 115 | 116 | // String implements the POSIX.1e short text form. 117 | // For example: 118 | // u::rwx,g::r-x,o::---,u:dvader:r--,m::r-- 119 | // This output is produced by an ACL in which the file owner 120 | // has read, write, and execute; the file group has read and 121 | // execute; other has no permissions; the user dvader has 122 | // read; and the mask is read. 123 | func (a ACL) String() string { 124 | strs := make([]string, len(a)) 125 | for i, e := range a { 126 | strs[i] = e.String() 127 | } 128 | return strings.Join(strs, ",") 129 | } 130 | 131 | // StringLong implements the POSIX.1e long text form. 132 | // The long text form of the example given above is: 133 | // user::rwx 134 | // group::r-x 135 | // other::--- 136 | // user:dvader:r-- 137 | // mask::r-- 138 | func (a ACL) StringLong() string { 139 | lines := make([]string, len(a)) 140 | mask := os.FileMode(7) 141 | for _, e := range a { 142 | if e.Tag == TagMask { 143 | mask = e.perms() 144 | break 145 | } 146 | } 147 | for i, e := range a { 148 | if (e.Tag == TagUser || e.Tag == TagGroupObj || e.Tag == TagGroup) && 149 | mask|e.perms() != mask { 150 | effective := mask & e.perms() 151 | lines[i] = fmt.Sprintf("%-20s#effective:%s", e.StringLong(), permString(effective)) 152 | } else { 153 | lines[i] = e.StringLong() 154 | } 155 | } 156 | return strings.Join(lines, "\n") 157 | } 158 | 159 | // Tag is the type of an ACL entry tag. 160 | type Tag tag 161 | 162 | const ( 163 | TagUserObj Tag = tagUserObj // Permissions of the file owner 164 | TagUser = tagUser // Permissions of a specified user 165 | TagGroupObj = tagGroupObj // Permissions of the file group 166 | TagGroup = tagGroup // Permissions of a specified group 167 | 168 | // Maximum allowed access rights of any entry 169 | // with the tag TagUser, TagGroupObj, or TagGroup 170 | TagMask = tagMask 171 | TagOther = tagOther // Permissions of a process not matching any other entry 172 | ) 173 | 174 | // String implements the POSIX.1e short text form. 175 | func (t Tag) String() string { 176 | switch t { 177 | case TagUser, TagUserObj: 178 | return "u" 179 | case TagGroup, TagGroupObj: 180 | return "g" 181 | case TagOther: 182 | return "o" 183 | case TagMask: 184 | return "m" 185 | default: 186 | // TODO(joshlf): what to do in this case? 187 | return "?" // non-standard, but not specified in POSIX.1e 188 | } 189 | } 190 | 191 | // StringLong implements the POSIX.1e long text form. 192 | func (t Tag) StringLong() string { 193 | switch t { 194 | case TagUser, TagUserObj: 195 | return "user" 196 | case TagGroup, TagGroupObj: 197 | return "group" 198 | case TagOther: 199 | return "other" 200 | case TagMask: 201 | return "mask" 202 | default: 203 | // TODO(joshlf): what to do in this case? 204 | return "????" // non-standard, but not specified in POSIX.1e 205 | } 206 | } 207 | 208 | // Entry represents an entry in an ACL. 209 | type Entry struct { 210 | Tag Tag 211 | 212 | // TODO(joshlf): it would be nice if we could handle 213 | // the UID/user name or GID/group name transition 214 | // transparently under the hood rather than pushing 215 | // the responsibility to the user. However, there are 216 | // some subtle considerations: 217 | // - It must be valid to provide a UID/GID for a 218 | // user or group that does not exist (setfactl 219 | // supports this) 220 | // - If the qualifier can be either a UID/GID or 221 | // a user name/group name, there should probably 222 | // be a better way of encoding it (that is, 223 | // better than just setting it to one or the 224 | // other and letting the user implement custom 225 | // logic to tell the difference) 226 | 227 | // The Qualifier specifies what entity (user or group) 228 | // this entry applies to. If the Tag is TagUser, it is 229 | // a UID; if the Tag is TagGroup, it is a GID; otherwise 230 | // the field is ignored. Note that the qualifier must 231 | // be a UID or GID - it cannot be, for example, a user name. 232 | Qualifier string 233 | 234 | // ACL permissions are taken from a traditional rwx 235 | // (read/write/execute) permissions vector. The Perms 236 | // field stores these as the lowest three bits - 237 | // the bits in any higher positions are ignored. 238 | Perms os.FileMode 239 | } 240 | 241 | // Use e.perms() to make sure that only 242 | // the lowest three bits are set - some 243 | // algorithms may inadvertently break 244 | // otherwise (including libacl itself). 245 | func (e Entry) perms() os.FileMode { return 7 & e.Perms } 246 | 247 | var permStrings = []string{ 248 | 0: "---", 249 | 1: "--x", 250 | 2: "-w-", 251 | 3: "-wx", 252 | 4: "r--", 253 | 5: "r-x", 254 | 6: "rw-", 255 | 7: "rwx", 256 | } 257 | 258 | // assumes perm has only lowest three bits set 259 | func permString(perm os.FileMode) string { 260 | return permStrings[int(perm)] 261 | } 262 | 263 | // String implements the POSIX.1e short text form. 264 | func (e Entry) String() string { 265 | middle := "::" 266 | if e.Tag == TagUser || e.Tag == TagGroup { 267 | middle = ":" + formatQualifier(e.Qualifier, e.Tag) + ":" 268 | } 269 | return fmt.Sprintf("%s%s%s", e.Tag, middle, permString(e.perms())) 270 | } 271 | 272 | // StringLong implements the POSIX.1e long text form. 273 | func (e Entry) StringLong() string { 274 | middle := "::" 275 | if e.Tag == TagUser || e.Tag == TagGroup { 276 | middle = ":" + formatQualifier(e.Qualifier, e.Tag) + ":" 277 | } 278 | return fmt.Sprintf("%s%s%s", e.Tag.StringLong(), middle, permString(e.perms())) 279 | } 280 | 281 | // overwrite in other files to implement platform-specific behavior 282 | var formatQualifier = func(q string, tag Tag) string { return q } 283 | 284 | // Get retrieves the access ACL associated with path, 285 | // returning any error encountered. 286 | func Get(path string) (ACL, error) { 287 | return get(path) 288 | } 289 | 290 | // FGet retrieves the access ACL associated with an *os.File, 291 | // returning any error encountered. 292 | func FGet(f *os.File) (ACL, error) { 293 | return fget(f) 294 | } 295 | 296 | // GetDefault retrieves the default ACL associated with path, 297 | // returning any error encountered. 298 | func GetDefault(path string) (ACL, error) { 299 | return getDefault(path) 300 | } 301 | 302 | // FGetDefault retrieves the default ACL associated with an *os.File, 303 | // returning any error encountered. 304 | func FGetDefault(f *os.File) (ACL, error) { 305 | return fgetDefault(f) 306 | } 307 | 308 | // Set sets the access ACL on path, 309 | // returning any error encountered. 310 | func Set(path string, acl ACL) error { 311 | if !acl.IsValid() { 312 | return fmt.Errorf("invalid ACL") 313 | } 314 | return set(path, acl) 315 | } 316 | 317 | // FSet sets the access ACL on an *os.File, 318 | // returning any error encountered. 319 | func FSet(f *os.File, acl ACL) error { 320 | if !acl.IsValid() { 321 | return fmt.Errorf("invalid ACL") 322 | } 323 | return fset(f, acl) 324 | } 325 | 326 | // SetDefault sets the default ACL on path, 327 | // returning any error encountered. 328 | func SetDefault(path string, acl ACL) error { 329 | if !acl.IsValid() { 330 | return fmt.Errorf("invalid ACL") 331 | } 332 | return setDefault(path, acl) 333 | } 334 | 335 | // FSetDefault sets the default ACL on an *os.File, 336 | // returning any error encountered. 337 | func FSetDefault(f *os.File, acl ACL) error { 338 | if !acl.IsValid() { 339 | return fmt.Errorf("invalid ACL") 340 | } 341 | return fsetDefault(f, acl) 342 | } 343 | 344 | func add(oldACL ACL, entries ...Entry) (newACL ACL, err error) { 345 | var ( 346 | addUserGroup bool // entries contains TagUser or TagGroup element 347 | addMask bool // entries contains TagMask element 348 | ) 349 | for _, e := range entries { 350 | switch e.Tag { 351 | case TagUser, TagGroup: 352 | addUserGroup = true 353 | case TagMask: 354 | addMask = true 355 | } 356 | } 357 | 358 | // put all of the entries into a map: first the 359 | // old entries, and then the new ones (so that 360 | // new entries overwrite old entries) 361 | type key struct { 362 | Tag Tag 363 | Qualifier string 364 | } 365 | m := make(map[key]Entry) 366 | for _, e := range oldACL { 367 | // we can rely on e.Qualifier to be the 368 | // empty string if e.Tag is neither TagUser 369 | // nor TagGroup (see the implementation of 370 | // get in acl_impl.go) 371 | m[key{e.Tag, e.Qualifier}] = e 372 | } 373 | for _, e := range entries { 374 | // the user could have passed an entry 375 | // whose Qualifier field was spuriously 376 | // non-empty; clean their input in case 377 | // this happened 378 | tag := e.Tag 379 | qual := e.Qualifier 380 | if tag != TagUser && tag != TagGroup { 381 | qual = "" 382 | } 383 | m[key{tag, qual}] = e 384 | } 385 | 386 | if addUserGroup && !addMask { 387 | // automatically add mask entry; 388 | // calculate its permissions to 389 | // be the union of all TagUser and 390 | // TagGroup permissions (see the 391 | // doc comment on this function) 392 | var mperms os.FileMode 393 | for _, e := range m { 394 | switch e.Tag { 395 | case TagUser, TagGroup, TagGroupObj: 396 | mperms |= e.Perms 397 | } 398 | } 399 | m[key{Tag: TagMask}] = Entry{Tag: TagMask, Perms: mperms} 400 | } 401 | 402 | for _, e := range m { 403 | newACL = append(newACL, e) 404 | } 405 | if !newACL.IsValid() { 406 | return newACL, errors.New("add results in invalid ACL") 407 | } 408 | return 409 | } 410 | 411 | // TODO(joshlf): It seems as though the mask also 412 | // affects entries with the tag TagGroupObj, so 413 | // when calculating the new mask, its bits should 414 | // be taken into account as well. 415 | 416 | // Add adds the given entries to the ACL on path. 417 | // Any matching entries that exist on the file 418 | // will be overwritten. Two entries match if they 419 | // have the same tag (and, if that tag is TagUser 420 | // or TagGroup, they also have the same qualifier). 421 | // 422 | // In order to ensure that the new ACL is valid, 423 | // after being calculated from the old ACL and the 424 | // new entries, the new ACL is modified as follows: 425 | // If the ACL includes named user or group entries 426 | // (with the tags TagUser or TagGroup) but no mask 427 | // entry, a mask entry is added. This entry's 428 | // permissions are the union of all permissions 429 | // affected by the entry (namely, all entries with 430 | // the tags TagUser, TagGroup, or TagGroupObj). 431 | func Add(path string, entries ...Entry) error { 432 | oldACL, err := get(path) 433 | if err != nil { 434 | return err 435 | } 436 | newACL, err := add(oldACL, entries...) 437 | if err != nil { 438 | return err 439 | } 440 | return set(path, newACL) 441 | } 442 | 443 | // FAdd adds the given entries to the ACL like Add, but on an *os.File 444 | func FAdd(f *os.File, entries ...Entry) error { 445 | oldACL, err := fget(f) 446 | if err != nil { 447 | return err 448 | } 449 | newACL, err := add(oldACL, entries...) 450 | if err != nil { 451 | return err 452 | } 453 | return fset(f, newACL) 454 | } 455 | --------------------------------------------------------------------------------