|
4 | 4 | -- License : BSD-3-Clause |
5 | 5 | -- Maintainer : streamly@composewell.com |
6 | 6 | -- Stability : experimental |
7 | | --- Portability : GHC |
| 7 | +-- Portability : GHC (POSIX only) |
8 | 8 | -- |
9 | | --- Experimental alternative wrapper API over System.Posix.User. |
| 9 | +-- Experimental alternative wrapper API over "System.Posix.User". |
10 | 10 | -- |
11 | | --- Shorter names, closer to shell commands. |
12 | | --- Int for user-id/group-id for covnenience. |
13 | | --- Adds one missing function |
| 11 | +-- Provides the read-only functionality of the @id@, @whoami@, and @logname@ |
| 12 | +-- coreutils commands, intended for programmatic use. |
14 | 13 | -- |
15 | | --- Functions to get and set the user and group id of the current process. |
| 14 | +-- = Design notes |
16 | 15 | -- |
17 | | --- Substitutes the functionality of the @id@ and @whoami@ coreutils commands. |
| 16 | +-- * __Scope: current process only.__ This module only reports identity |
| 17 | +-- information about the /current process/. Looking up identity details for |
| 18 | +-- an arbitrary named user (i.e. @id \<username\>@) is a separate concern |
| 19 | +-- that requires reading the user/group database (@/etc/passwd@, |
| 20 | +-- @/etc/group@, NSS, etc.). That functionality will live in a separate |
| 21 | +-- module (e.g. @Streamly.Coreutils.UserDB@) and is not implemented here. |
18 | 22 | -- |
19 | | --- This is a Posix only module. |
20 | | - |
21 | | --- TODO: create a portable module with "idNum" and "idName" commands to print |
22 | | --- the current user id, name. |
| 23 | +-- * __Scope: read-only.__ Setting the uid/gid of the current process |
| 24 | +-- (@setuid@, @setgid@) is the domain of @sudo@-style utilities and has |
| 25 | +-- significant security implications. It is intentionally not exposed from |
| 26 | +-- this module. |
| 27 | +-- |
| 28 | +-- * __Portability.__ This is a POSIX-only module. A portable alternative |
| 29 | +-- exposing a minimal common subset (e.g. @idNum@, @idName@) could be added |
| 30 | +-- later. |
| 31 | +-- |
| 32 | +-- * __Int for ids.__ User and group ids are exposed as 'Int' for |
| 33 | +-- convenience and brevity at call sites. Alternatives considered: |
| 34 | +-- 'System.Posix.Types.UserID' / 'System.Posix.Types.GroupID' (which are |
| 35 | +-- @CUid@ / @CGid@ newtypes) would be more type-safe (a uid cannot be |
| 36 | +-- confused with a gid) and avoid any downcast concerns on platforms where |
| 37 | +-- these are wider than 'Int'. If type safety or exotic-platform |
| 38 | +-- correctness becomes a priority, switch to those. Downcasts via |
| 39 | +-- 'fromIntegral' are safe on all mainstream 64-bit platforms where |
| 40 | +-- uid_t/gid_t are 32 bits. |
| 41 | +-- |
| 42 | +-- * __'Maybe' for DB lookups.__ Functions that may fail to find a matching |
| 43 | +-- entry return 'Maybe' rather than throwing, which is friendlier for |
| 44 | +-- callers. The underlying @System.Posix.User@ primitives throw on a miss; |
| 45 | +-- we catch and convert. |
| 46 | +-- |
| 47 | +-- * __Individual functions over a bundled record.__ Each piece of |
| 48 | +-- information is exposed as its own function rather than a combined |
| 49 | +-- @IdInfo@ record. A record would not save syscalls here (each field |
| 50 | +-- corresponds to a distinct primitive), so the extra surface area isn't |
| 51 | +-- justified. Revisit if a batched primitive becomes available. |
| 52 | +-- |
| 53 | +-- * __@whoami@ is just @effectiveUserName@__ and is not exposed as a |
| 54 | +-- separate function to avoid redundancy. |
23 | 55 | -- |
24 | | --- Can separate process API and the user DB API. |
25 | | - |
26 | 56 | module Streamly.Coreutils.Id |
27 | 57 | ( |
28 | | - uid |
29 | | - , euid |
30 | | - , gid |
31 | | - , egid |
32 | | - , uid2name |
33 | | - , gid2name |
34 | | - -- , groups |
| 58 | + -- * Numeric ids of the current process |
| 59 | + realUserId |
| 60 | + , effectiveUserId |
| 61 | + , realGroupId |
| 62 | + , effectiveGroupId |
| 63 | + , groupIds |
| 64 | + |
| 65 | + -- * Names for the current process |
| 66 | + , realUserName |
| 67 | + , effectiveUserName |
| 68 | + , realGroupName |
| 69 | + , effectiveGroupName |
| 70 | + , groupNames |
| 71 | + , loginName |
35 | 72 | ) |
36 | 73 | where |
37 | 74 |
|
38 | | -import System.Posix (getGroupEntryForID) |
39 | | -import Prelude hiding (id) |
| 75 | +import Control.Exception (try, SomeException) |
| 76 | +import Data.List (nub) |
40 | 77 | import qualified System.Posix.User as Posix |
41 | 78 |
|
42 | | ------------------------------------------------------- |
43 | | --- Current process settings |
44 | | ------------------------------------------------------- |
| 79 | +------------------------------------------------------------------------------ |
| 80 | +-- Internal helpers |
| 81 | +------------------------------------------------------------------------------ |
45 | 82 |
|
46 | | --- | Return current process real user id. |
| 83 | +-- Look up a user-db entry by id, returning Nothing if it doesn't exist rather |
| 84 | +-- than throwing. See "Maybe for DB lookups" in the module header. |
47 | 85 | -- |
48 | | --- id -ru |
49 | | -uid :: IO Int |
50 | | -uid = fromIntegral <$> Posix.getRealUserID |
51 | | - |
52 | | --- | Return current process real group id. |
| 86 | +-- Type signatures are intentionally omitted: UserEntry and GroupEntry are |
| 87 | +-- defined in an internal module of the unix package and are not re-exported |
| 88 | +-- from System.Posix.User, so they cannot be named here without pulling in |
| 89 | +-- the internal module. The inferred types are correct. |
| 90 | +-- tryLookupUser :: Int -> IO (Maybe UserEntry) |
| 91 | +tryLookupUser i = do |
| 92 | + r <- try (Posix.getUserEntryForID (fromIntegral (i :: Int))) |
| 93 | + return $ case r of |
| 94 | + Left (_ :: SomeException) -> Nothing |
| 95 | + Right ue -> Just ue |
| 96 | + |
| 97 | +-- tryLookupGroup :: Int -> IO (Maybe GroupEntry) |
| 98 | +tryLookupGroup i = do |
| 99 | + r <- try (Posix.getGroupEntryForID (fromIntegral (i :: Int))) |
| 100 | + return $ case r of |
| 101 | + Left (_ :: SomeException) -> Nothing |
| 102 | + Right ge -> Just ge |
| 103 | + |
| 104 | +------------------------------------------------------------------------------ |
| 105 | +-- Current process: numeric ids |
| 106 | +------------------------------------------------------------------------------ |
| 107 | + |
| 108 | +-- | Real user id of the current process. Corresponds to @id -ru@. |
| 109 | +realUserId :: IO Int |
| 110 | +realUserId = fromIntegral <$> Posix.getRealUserID |
| 111 | + |
| 112 | +-- | Effective user id of the current process. Corresponds to @id -u@. |
| 113 | +effectiveUserId :: IO Int |
| 114 | +effectiveUserId = fromIntegral <$> Posix.getEffectiveUserID |
| 115 | + |
| 116 | +-- | Real group id of the current process. Corresponds to @id -rg@. |
| 117 | +realGroupId :: IO Int |
| 118 | +realGroupId = fromIntegral <$> Posix.getRealGroupID |
| 119 | + |
| 120 | +-- | Effective group id of the current process. Corresponds to @id -g@. |
| 121 | +effectiveGroupId :: IO Int |
| 122 | +effectiveGroupId = fromIntegral <$> Posix.getEffectiveGroupID |
| 123 | + |
| 124 | +-- | All group ids the current process belongs to: the effective primary |
| 125 | +-- group plus all supplementary groups, deduplicated. Corresponds to @id -G@. |
53 | 126 | -- |
54 | | --- id -rg |
55 | | -gid :: IO Int |
56 | | -gid = fromIntegral <$> Posix.getRealGroupID |
57 | | - |
58 | | --- | Return current process effective user id. |
| 127 | +-- Note: @getgroups(2)@ alone returns only the supplementary list, and |
| 128 | +-- whether that list includes the primary gid is OS-dependent. This function |
| 129 | +-- explicitly merges the primary gid with the supplementary list to match |
| 130 | +-- the @id -G@ command's output. |
| 131 | +groupIds :: IO [Int] |
| 132 | +groupIds = do |
| 133 | + primary <- effectiveGroupId |
| 134 | + supp <- map fromIntegral <$> Posix.getGroups |
| 135 | + return $ nub (primary : supp) |
| 136 | + |
| 137 | +------------------------------------------------------------------------------ |
| 138 | +-- Current process: names |
| 139 | +------------------------------------------------------------------------------ |
| 140 | + |
| 141 | +-- | Real user name of the current process. Corresponds to @id -unr@. |
59 | 142 | -- |
60 | | --- id -u |
61 | | -euid :: IO Int |
62 | | -euid = fromIntegral <$> Posix.getEffectiveUserID |
| 143 | +-- 'Nothing' if there is no user-db entry for the real uid. |
| 144 | +realUserName :: IO (Maybe String) |
| 145 | +realUserName = realUserId >>= fmap (fmap Posix.userName) . tryLookupUser |
63 | 146 |
|
64 | | --- | Return current process effective group id. |
| 147 | +-- | Effective user name of the current process. Corresponds to @id -un@ |
| 148 | +-- and @whoami@. |
65 | 149 | -- |
66 | | --- id -g |
67 | | -egid :: IO Int |
68 | | -egid = fromIntegral <$> Posix.getEffectiveGroupID |
| 150 | +-- 'Nothing' if there is no user-db entry for the effective uid. |
| 151 | +effectiveUserName :: IO (Maybe String) |
| 152 | +effectiveUserName = |
| 153 | + effectiveUserId >>= fmap (fmap Posix.userName) . tryLookupUser |
69 | 154 |
|
70 | | --- | Get groups associated with the current process. |
| 155 | +-- | Real group name of the current process. Corresponds to @id -gnr@. |
71 | 156 | -- |
72 | | --- id -G |
73 | | -groups :: IO [Int] |
74 | | -groups = fmap fromIntegral <$> Posix.getGroups |
75 | | - |
76 | | --- | The original login name of the process. Note: the current user name may |
77 | | --- change by setuid but login name remains the same. |
78 | | -logname :: IO String |
79 | | -logname = Posix.getLoginName |
80 | | - |
81 | | ------------------------------------------------------- |
82 | | --- These should go to user database module? |
83 | | ------------------------------------------------------- |
84 | | - |
85 | | --- XXX We can parse the passwd file ourselves instead of using C code |
86 | | --- XXX Use an Compact Array/OsString instead? |
87 | | - |
88 | | -{- |
89 | | --- getpwuid |
90 | | -uid2pwent = |
| 157 | +-- 'Nothing' if there is no group-db entry for the real gid. |
| 158 | +realGroupName :: IO (Maybe String) |
| 159 | +realGroupName = realGroupId >>= fmap (fmap Posix.groupName) . tryLookupGroup |
91 | 160 |
|
92 | | --- getgrgid |
93 | | -gid2grent = |
94 | | -
|
95 | | --- getpwnam |
96 | | -name2pwent = |
97 | | -
|
98 | | --- getgrnam |
99 | | -name2grent = |
100 | | -
|
101 | | --- Stream the entries. |
102 | | -getpwents = |
103 | | --} |
104 | | - |
105 | | --- | Convert numeric user id to user name. |
106 | | -uid2name :: Int -> IO String |
107 | | -uid2name i = do |
108 | | - -- XXX fromIntegral downcast |
109 | | - pw <- Posix.getUserEntryForID (fromIntegral i) |
110 | | - return (Posix.userName pw) |
111 | | - |
112 | | --- XXX Use an Compact Array/OsString instead? |
113 | | - |
114 | | --- | Convert numeric group id to group name. |
115 | | -gid2name :: Int -> IO String |
116 | | -gid2name i = do |
117 | | - -- XXX fromIntegral downcast |
118 | | - pw <- Posix.getGroupEntryForID (fromIntegral i) |
119 | | - return (Posix.groupName pw) |
120 | | - |
121 | | --- Note there is no uid2groups as anyway the groups file has the user name. We |
122 | | --- search by name even if uid is provided. So we can convert uid to name and |
123 | | --- then search. |
124 | | - |
125 | | --- | List all the groups in which a uid occurs. Returns (gid, group name). |
126 | | --- |
127 | | --- id -Gn <user> |
128 | | -user2groups :: Int -> [(Int, String)] |
129 | | -user2groups = undefined |
130 | | - |
131 | | --- | Current process user name. |
| 161 | +-- | Effective group name of the current process. Corresponds to @id -gn@. |
132 | 162 | -- |
133 | | --- id -un |
134 | | -whoami :: IO String |
135 | | -whoami = uid >>= uid2name |
| 163 | +-- 'Nothing' if there is no group-db entry for the effective gid. |
| 164 | +effectiveGroupName :: IO (Maybe String) |
| 165 | +effectiveGroupName = |
| 166 | + effectiveGroupId >>= fmap (fmap Posix.groupName) . tryLookupGroup |
136 | 167 |
|
137 | | --- | Current process group names. |
| 168 | +-- | Names of all groups the current process belongs to, in the same order |
| 169 | +-- as 'groupIds'. Corresponds to @id -Gn@. |
138 | 170 | -- |
139 | | --- id -Gn |
| 171 | +-- Entries for which no group-db record exists are silently dropped. |
140 | 172 | groupNames :: IO [String] |
141 | 173 | groupNames = do |
142 | | - xs <- Posix.getGroups |
143 | | - fmap Posix.groupName <$> mapM getGroupEntryForID xs |
| 174 | + gs <- groupIds |
| 175 | + mEntries <- mapM tryLookupGroup gs |
| 176 | + return [ Posix.groupName ge | Just ge <- mEntries ] |
| 177 | + |
| 178 | +-- | Original login name of the session the current process belongs to. |
| 179 | +-- |
| 180 | +-- Note: this can differ from 'effectiveUserName' after operations like |
| 181 | +-- @su@ or @setuid@ — the login name reflects who originally logged in, |
| 182 | +-- not who the process is currently acting as. Corresponds to the |
| 183 | +-- @logname@ command. |
| 184 | +loginName :: IO String |
| 185 | +loginName = Posix.getLoginName |
0 commit comments