Skip to content

Commit 26bd9c9

Browse files
Improve the Id module, first iteration
1 parent e160279 commit 26bd9c9

2 files changed

Lines changed: 153 additions & 111 deletions

File tree

src/Streamly/Coreutils/Id.hs

Lines changed: 152 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -4,140 +4,182 @@
44
-- License : BSD-3-Clause
55
-- Maintainer : streamly@composewell.com
66
-- Stability : experimental
7-
-- Portability : GHC
7+
-- Portability : GHC (POSIX only)
88
--
9-
-- Experimental alternative wrapper API over System.Posix.User.
9+
-- Experimental alternative wrapper API over "System.Posix.User".
1010
--
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.
1413
--
15-
-- Functions to get and set the user and group id of the current process.
14+
-- = Design notes
1615
--
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.
1822
--
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.
2355
--
24-
-- Can separate process API and the user DB API.
25-
2656
module Streamly.Coreutils.Id
2757
(
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
3572
)
3673
where
3774

38-
import System.Posix (getGroupEntryForID)
39-
import Prelude hiding (id)
75+
import Control.Exception (try, SomeException)
76+
import Data.List (nub)
4077
import qualified System.Posix.User as Posix
4178

42-
------------------------------------------------------
43-
-- Current process settings
44-
------------------------------------------------------
79+
------------------------------------------------------------------------------
80+
-- Internal helpers
81+
------------------------------------------------------------------------------
4582

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.
4785
--
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@.
53126
--
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@.
59142
--
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
63146

64-
-- | Return current process effective group id.
147+
-- | Effective user name of the current process. Corresponds to @id -un@
148+
-- and @whoami@.
65149
--
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
69154

70-
-- | Get groups associated with the current process.
155+
-- | Real group name of the current process. Corresponds to @id -gnr@.
71156
--
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
91160

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@.
132162
--
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
136167

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@.
138170
--
139-
-- id -Gn
171+
-- Entries for which no group-db record exists are silently dropped.
140172
groupNames :: IO [String]
141173
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

streamly-coreutils.cabal

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,6 @@ library
124124
, Streamly.Coreutils.Directory
125125
, Streamly.Coreutils.Dirname
126126
, Streamly.Coreutils.FileTest
127-
, Streamly.Coreutils.Id
128127
, Streamly.Coreutils.Ln
129128
, Streamly.Coreutils.Cut
130129
, Streamly.Coreutils.Ls
@@ -145,6 +144,7 @@ library
145144
else
146145
exposed-modules:
147146
Streamly.Coreutils.FileTest.Posix
147+
, Streamly.Coreutils.Id
148148
other-modules:
149149
Streamly.Coreutils.String
150150
, Streamly.Coreutils.Uniq

0 commit comments

Comments
 (0)