summaryrefslogtreecommitdiff
path: root/internal/user/server/user.go
blob: 332ea960305da7f4f1db08406a68549c4307df05 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
package server

import (
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"strings"

	"github.com/mimecast/dtail/internal/config"
	"github.com/mimecast/dtail/internal/io/dlog"
	"github.com/mimecast/dtail/internal/io/fs/permissions"
)

const maxLinkDepth int = 100

// User represents an end-user which connected to the server via the DTail client.
type User struct {
	// The user name.
	Name string
	// The remote address connected from.
	remoteAddress string
	// The permissions the user has.
	permissions []string
}

// PermissionLookup resolves permissions for a given SSH user.
type PermissionLookup func(string) ([]string, error)

// New returns a new user.
func New(name, remoteAddress string, permissionLookup PermissionLookup) (*User, error) {
	var (
		permissions []string
		err         error
	)
	if permissionLookup != nil {
		permissions, err = permissionLookup(name)
		if err != nil {
			return nil, err
		}
	}
	return &User{
		Name:          name,
		remoteAddress: remoteAddress,
		permissions:   permissions,
	}, nil
}

// String representation of the user.
func (u *User) String() string {
	return fmt.Sprintf("%s@%s", u.Name, u.remoteAddress)
}

// HasFilePermission is used to determine whether user is allowed to read a file.
func (u *User) HasFilePermission(filePath, permissionType string) (hasPermission bool) {
	dlog.Server.Debug(u, filePath, permissionType, "Checking config permissions")
	if u.Name == config.ScheduleUser || u.Name == config.ContinuousUser {
		// Background user has same permissions as dtail process itself.
		return true
	}

	cleanPath, err := filepath.EvalSymlinks(filePath)
	if err != nil {
		dlog.Server.Error(u, filePath, permissionType,
			"Unable to evaluate symlinks", err)
		hasPermission = false
		return
	}

	cleanPath, err = filepath.Abs(cleanPath)
	if err != nil {
		dlog.Server.Error(u, cleanPath, permissionType,
			"Unable to make file path absolute", err)
		hasPermission = false
		return
	}

	if cleanPath != filePath {
		dlog.Server.Info(u, filePath, cleanPath, permissionType,
			"Calculated new clean path from original file path (possibly symlink)")
	}

	hasPermission, err = u.hasFilePermission(cleanPath, permissionType)
	if err != nil {
		dlog.Server.Warn(u, cleanPath, err)
	}
	return
}

func (u *User) hasFilePermission(cleanPath, permissionType string) (bool, error) {
	// First check file system Linux/UNIX permission.
	if _, err := permissions.ToRead(u.Name, cleanPath); err != nil {
		return false, fmt.Errorf("User without OS file system permissions to read path: %w", err)
	}
	dlog.Server.Info(u, cleanPath, permissionType,
		"User with OS file system permissions to path")

	// Only allow to follow regular files or symlinks.
	info, err := os.Lstat(cleanPath)
	if err != nil {
		return false, fmt.Errorf("Unable to determine file type: %w", err)
	}
	if !info.Mode().IsRegular() {
		return false, fmt.Errorf("Can only open regular files or follow symlinks")
	}
	hasPermission, err := u.iteratePaths(cleanPath, permissionType)
	if err != nil {
		return false, err
	}

	return hasPermission, nil
}

func (u *User) iteratePaths(cleanPath, permissionType string) (bool, error) {
	// By default assume no permissions
	hasPermission := false
	for _, permission := range u.permissions {
		typeStr := "readfiles" // Assume ReadFiles by default.
		var regexStr string
		var negate bool

		splitted := strings.Split(permission, ":")
		if len(splitted) > 1 {
			typeStr = splitted[0]
			permission = strings.Join(splitted[1:], ":")
		}

		dlog.Server.Debug(u, cleanPath, typeStr, permission)
		if typeStr != permissionType {
			continue
		}

		regexStr = permission
		if strings.HasPrefix(permission, "!") {
			regexStr = permission[1:]
			negate = true
		}

		re, err := regexp.Compile(regexStr)
		if err != nil {
			return false, fmt.Errorf("Permission test failed, can't compile regex "+
				"'%s': %w", regexStr, err)
		}
		if negate && re.MatchString(cleanPath) {
			dlog.Server.Info(u, cleanPath, "Permission test failed partially, "+
				"matching negative pattern '%s'", permission)
			hasPermission = false
		}
		if !negate && re.MatchString(cleanPath) {
			dlog.Server.Info(u, cleanPath, "Permission test passed partially, "+
				"matching positive pattern", permission)
			hasPermission = true
		}
	}

	return hasPermission, nil
}