File size: 5,325 Bytes
6d57fe0
4b8758b
6d57fe0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4b8758b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6d57fe0
4b8758b
6d57fe0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
use super::perms::{check_permission, Access};
use super::{Gid, Uid, ROOT_UID, WHEEL_GID};
use crate::fs::inode::Inode;

/// Context for the user an agent is acting on behalf of.
#[derive(Debug, Clone)]
pub struct DelegateContext {
    pub uid: Uid,
    pub gid: Gid,
    pub groups: Vec<Gid>,
    pub username: String,
}

#[derive(Debug, Clone)]
pub struct Session {
    pub uid: Uid,
    pub gid: Gid,
    pub groups: Vec<Gid>,
    pub username: String,
    /// When set, the session acts on behalf of this user.
    /// All permission checks require BOTH the principal AND
    /// the delegate to have access (intersection / least-privilege).
    pub delegate: Option<DelegateContext>,
}

impl Session {
    pub fn new(uid: Uid, gid: Gid, groups: Vec<Gid>, username: String) -> Self {
        Session {
            uid,
            gid,
            groups,
            username,
            delegate: None,
        }
    }

    pub fn root() -> Self {
        Session {
            uid: ROOT_UID,
            gid: 0,
            groups: vec![0, 1],
            username: "root".to_string(),
            delegate: None,
        }
    }

    pub fn is_root(&self) -> bool {
        self.uid == ROOT_UID
    }

    /// Check permission with delegation intersection.
    /// Returns true only if both the principal AND the delegate (if any) have access.
    pub fn has_permission(&self, inode: &Inode, access: Access) -> bool {
        if !check_permission(inode, self.uid, &self.groups, access) {
            return false;
        }
        if let Some(ref delegate) = self.delegate {
            if !check_permission(inode, delegate.uid, &delegate.groups, access) {
                return false;
            }
        }
        true
    }

    /// Check permission using raw mode/uid/gid bits (for LsEntry filtering).
    /// Respects delegation intersection.
    pub fn has_permission_bits(
        &self,
        mode: u16,
        file_uid: Uid,
        file_gid: Gid,
        access: Access,
    ) -> bool {
        if !check_bits(self.uid, &self.groups, mode, file_uid, file_gid, access) {
            return false;
        }
        if let Some(ref delegate) = self.delegate {
            if !check_bits(
                delegate.uid,
                &delegate.groups,
                mode,
                file_uid,
                file_gid,
                access,
            ) {
                return false;
            }
        }
        true
    }

    /// The effective uid for file ownership — if delegating, use the delegate's uid.
    pub fn effective_uid(&self) -> Uid {
        match &self.delegate {
            Some(d) => d.uid,
            None => self.uid,
        }
    }

    /// The effective gid for file ownership — if delegating, use the delegate's gid.
    pub fn effective_gid(&self) -> Gid {
        match &self.delegate {
            Some(d) => d.gid,
            None => self.gid,
        }
    }

    /// Whether the principal is a wheel-group member (admin-equivalent).
    pub fn is_wheel(&self) -> bool {
        self.groups.contains(&WHEEL_GID)
    }

    /// Whether the session may bypass per-file Read checks (i.e., see every
    /// directory entry and read every file). True for literal uid=0 or any
    /// wheel-group member. Privileged write ops (chmod-across-owner,
    /// chown-uid) still go through the stricter `is_effectively_root` check.
    /// When delegating, the delegate must also be admin-equivalent.
    pub fn can_read_anywhere(&self) -> bool {
        let principal_admin = self.uid == ROOT_UID || self.is_wheel();
        if !principal_admin {
            return false;
        }
        match &self.delegate {
            Some(d) => d.uid == ROOT_UID || d.groups.contains(&WHEEL_GID),
            None => true,
        }
    }

    /// Whether either the principal or (if delegating) the delegate is root.
    /// Strict: only literal uid=0 qualifies. Used by privileged write ops.
    pub fn is_effectively_root(&self) -> bool {
        if self.uid != ROOT_UID {
            return false;
        }
        match &self.delegate {
            Some(d) => d.uid == ROOT_UID,
            None => true,
        }
    }

    /// Whether the principal (ignoring delegation) is the owner of a file.
    /// When delegating, checks if the delegate is the owner.
    pub fn is_effective_owner(&self, file_uid: Uid) -> bool {
        match &self.delegate {
            Some(d) => {
                // Both must be owner or root
                let principal_ok = self.uid == ROOT_UID || self.uid == file_uid;
                let delegate_ok = d.uid == ROOT_UID || d.uid == file_uid;
                principal_ok && delegate_ok
            }
            None => self.uid == ROOT_UID || self.uid == file_uid,
        }
    }
}

/// Raw bit-level permission check for a single principal.
fn check_bits(
    uid: Uid,
    groups: &[Gid],
    mode: u16,
    file_uid: Uid,
    file_gid: Gid,
    access: Access,
) -> bool {
    if uid == ROOT_UID {
        return true;
    }
    let bit = match access {
        Access::Read => 4,
        Access::Write => 2,
        Access::Execute => 1,
    };
    if uid == file_uid {
        return (mode >> 6) & bit != 0;
    }
    if groups.contains(&file_gid) {
        return (mode >> 3) & bit != 0;
    }
    mode & bit != 0
}