User permission checking in Go
Tags: go, perm, file
Published: 2022-11-12 21:14:30

<small>This article was [originally published](https://pebcak.de/posts/2022/11/12/check-file-perms-in-go/) on my personal blog.</small>

## The problem

While working on my [go-pf](https://github.com/wneessen/go-pf) module, I was faced with the problem to make sure that the `/dev/pf` device is readable and writable. While other languages like the Shell or Perl offer test functions to check if the current user/process has read/write/exec permissions on the specific file, in Go this task is not trivial.

Here is how we can do it with Perl:
```perl
#!/usr/bin/env perl

my $file = "test.txt";
if(!-r $file || !-w $file) {
    print $file . " is not read- and/or writable to the process\n";
    exit(1);
}
print "We're all good!\n"
```

If we run a quick one-liner in the Shell then, we can see how the script works:
```shell
$ for i in 100 200 400 500 600 700; do echo "chmod: $i"; \
    chmod $i test.txt; perl test.pl; done
chmod: 100
test.txt is not read- and/or writable to the process
chmod: 200
test.txt is not read- and/or writable to the process
chmod: 400
test.txt is not read- and/or writable to the process
chmod: 500
test.txt is not read- and/or writable to the process
chmod: 600
We're all good!
chmod: 700
We're all good!
```

In Go we would need to rely on [os.Lstat](https://pkg.go.dev/os#Lstat) which would then provide us with the `Mode().Perm()` method, which we could use to get the file mode of the file. But that is only half the rent. Just because the file might return a file mode of `700`, which basically would mean that read and write permissions are given, the file might not be owned by us. It gets even more complicated when we need to take in account that we also have group and "other" file permissions. 

## The solution

Since I couldn't find a ready-made solution, I made it my task to solve the problem - preferably in an equally simple way, such as in Perl.

As mentioned, we can utilize `os.Lstat` as our starting point. With the file permissions and some bit shifting, we can easily figure out if a file is read/write/executable for "other", group and user. With this information at hand, the next step is to figure out the owner and the group of the file in question. For UNIX-like operating systems, Go provides us with the `Sys()` method on our `FileInfo` that is returned by `os.Lstat`. We can try to type-cast the result of `Sys()` into a [*syscall.Stat_t](https://pkg.go.dev/syscall#Stat_t) pointer. If this succeeds, we will get the information that we need with the `Uid` and `Gid` properties. 

Problem number one is solved, we can see who owns the file and check if our current user is indeed the owner. The [os/user.Current](https://pkg.go.dev/os/user#Current) method can help us with that. Our next task is group ownership - again this is not a trivial task, since a user can be part of several groups. Again the Go standard library provides us with the tools we need in form of the [User.GroupIds](https://pkg.go.dev/os/user#User.GroupIds) method, which will give us a list of groups that our user is part of. With this list, it is fairly easy to figure out if our user is part of the group of the file. The second problem is solved as well. For the "other" permission we can again work off with some bit-shifting. 

## go-fileperm

You probably guessed already, that I built something out of all of this. [go-fileperm](https//github.com/wneessen/go-fileperm) uses all the information I discussed before into one simple-to-use Go module. Let's have a look.

First of all, we use the `New()` method to get an instance of the type `UserPerm`. `New` takes a string as an argument, which is the path to the file in question. If this succeeds, the module will provide you with a couple of methods to check the file access permissions (all of which return a boolean value):

* `UserExecutable()`: returns true if the current user/process has execution permission on the file in question.
* `UserWritable()`: returns true if the current user/process has write permission on the file in question.
* `UserReadable()`: returns true if the current user/process has read permission on the file in question.
* `UserWriteExecutable()`: returns true if the current user/process has write and execution permission on the 
  file in question.
* `UserReadExecutable()`: returns true if the current user/process has read and execution permission on the
  file in question.
* `UserWriteReadExecutable()`: returns true if the current user/process has read, write and execution permission 
  on the file in question.

## Performance

Since go-fileperm mostly makes use of bit-shifting, the performance of the module is pretty fast. Also, we work allocation-free, which is always a plus :)

```
goos: darwin
goarch: arm64
pkg: github.com/wneessen/go-fileperm
BenchmarkPermUser_UserReadable
BenchmarkPermUser_UserReadable-8                 7364846               143.6 ns/op             0 B/op          0 allocs/op
BenchmarkPermUser_UserWritable
BenchmarkPermUser_UserWritable-8                 7803267               154.9 ns/op             0 B/op          0 allocs/op
BenchmarkPermUser_UserExecutable
BenchmarkPermUser_UserExecutable-8               7922624               149.2 ns/op             0 B/op          0 allocs/op
BenchmarkPermUser_UserWriteReadable
BenchmarkPermUser_UserWriteReadable-8            6494815               186.1 ns/op             0 B/op          0 allocs/op
BenchmarkPermUser_UserWriteExecutable
BenchmarkPermUser_UserWriteExecutable-8          6590229               181.0 ns/op             0 B/op          0 allocs/op
BenchmarkPermUser_UserReadExecutable
BenchmarkPermUser_UserReadExecutable-8           6190532               184.7 ns/op             0 B/op          0 allocs/op
BenchmarkPermUser_UserWriteReadExecutable
BenchmarkPermUser_UserWriteReadExecutable-8      5728713               208.8 ns/op             0 B/op          0 allocs/op
PASS
```

## Full code example

Let's check out a full code example and apply our one-liner that we used in the beginning.

```go
package main

import (
    "fmt"
    "log"

    "github.com/wneessen/go-fileperm"
)

func main() {
    p, err := fileperm.New("test.txt")
    if err != nil {
        log.Fatalf("failed to create UserPerm instance: %s", err)
    }
    fmt.Printf("test.txt is user-readable:              %t\n", p.UserReadable())
    fmt.Printf("test.txt is user-writable:              %t\n", p.UserWritable())
    fmt.Printf("test.txt is user-executable:            %t\n", p.UserExecutable())
    fmt.Printf("test.txt is user-read/writable:         %t\n", p.UserWriteReadable())
    fmt.Printf("test.txt is user-read/executable:       %t\n", p.UserReadExecutable())
    fmt.Printf("test.txt is user-write/executable:      %t\n", p.UserWriteExecutable())
    fmt.Printf("test.txt is user-read/write/executable: %t\n", p.UserWriteReadExecutable())
}
```

```shell
$ for i in 100 200 400 500 600 700; do echo "chmod: $i"; \
    chmod $i test.txt; go run main.go; done

chmod: 100
test.txt is user-readable:              false
test.txt is user-writable:              false
test.txt is user-executable:            true
test.txt is user-read/writable:         false
test.txt is user-read/executable:       false
test.txt is user-write/executable:      false
test.txt is user-read/write/executable: false
chmod: 200
test.txt is user-readable:              false
test.txt is user-writable:              true
test.txt is user-executable:            false
test.txt is user-read/writable:         false
test.txt is user-read/executable:       false
test.txt is user-write/executable:      false
test.txt is user-read/write/executable: false
chmod: 400
test.txt is user-readable:              true
test.txt is user-writable:              false
test.txt is user-executable:            false
test.txt is user-read/writable:         false
test.txt is user-read/executable:       false
test.txt is user-write/executable:      false
test.txt is user-read/write/executable: false
chmod: 500
test.txt is user-readable:              true
test.txt is user-writable:              false
test.txt is user-executable:            true
test.txt is user-read/writable:         false
test.txt is user-read/executable:       true
test.txt is user-write/executable:      false
test.txt is user-read/write/executable: false
chmod: 600
test.txt is user-readable:              true
test.txt is user-writable:              true
test.txt is user-executable:            false
test.txt is user-read/writable:         true
test.txt is user-read/executable:       false
test.txt is user-write/executable:      false
test.txt is user-read/write/executable: false
chmod: 700
test.txt is user-readable:              true
test.txt is user-writable:              true
test.txt is user-executable:            true
test.txt is user-read/writable:         true
test.txt is user-read/executable:       true
test.txt is user-write/executable:      true
test.txt is user-read/write/executable: true
```

## Conclusion

As you can see, the module is pretty easy to use and hopefully will be a helpful tool for some of you. As usual, I've published it on GitHub under the MIT license, so feel free to give it a try or even contribute if you think there is things that need improvements.