blob: 584ce9a692d589efbc78b271274d260654c35b08 [file] [log] [blame]
# -*- coding: utf-8 -*-
# Copyright 2015 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Logic to read the set of users and groups installed on a system."""
from __future__ import print_function
import collections
import os
from chromite.lib import cros_logging as logging
from chromite.lib import locking
from chromite.lib import osutils
# These fields must be in the order expected in /etc/passwd entries.
User = collections.namedtuple(
'User', ('user', 'password', 'uid', 'gid', 'gecos', 'home', 'shell'))
# These fields must be in the order expected in /etc/group entries.
Group = collections.namedtuple(
'Group', ('group', 'password', 'gid', 'users'))
def UserToEntry(user):
"""Returns the database file entry corresponding to |user|."""
return ':'.join([user.user, user.password, str(user.uid), str(user.gid),
user.gecos, user.home, user.shell])
def GroupToEntry(group):
"""Returns the database file entry corresponding to |group|."""
return ':'.join([group.group, group.password,
str(group.gid), ','.join(group.users)])
class UserDB(object):
"""An object that understands the users and groups installed on a system."""
# Number of times to attempt to acquire the write lock on a database.
# The max wait time for the lock is the nth triangular number of seconds.
# So in this case, T(24) * 1 second = 300 seconds.
_DB_LOCK_RETRIES = 24
def __init__(self, sysroot):
self._sysroot = sysroot
self._user_cache = None
self._group_cache = None
@property
def _user_db_file(self):
"""Returns path to user database (aka /etc/passwd in the sysroot)."""
return os.path.join(self._sysroot, 'etc', 'passwd')
@property
def _group_db_file(self):
"""Returns path to group database (aka /etc/group in the sysroot)."""
return os.path.join(self._sysroot, 'etc', 'group')
@property
def _users(self):
"""Returns a list of User tuples."""
if self._user_cache is not None:
return self._user_cache
self._user_cache = {}
passwd_contents = osutils.ReadFile(self._user_db_file)
for line in passwd_contents.splitlines():
pieces = line.split(':')
if len(pieces) != 7:
logging.warning('Ignored invalid line in users file: "%s"', line)
continue
user, password, uid, gid, gecos, home, shell = pieces
try:
uid_as_int = int(uid)
gid_as_int = int(gid)
except ValueError:
logging.warning('Ignored invalid uid (%s) or gid (%s).', uid, gid)
continue
if user in self._user_cache:
logging.warning('Ignored duplicate user definition for "%s".', user)
continue
self._user_cache[user] = User(user=user, password=password,
uid=uid_as_int, gid=gid_as_int,
gecos=gecos, home=home, shell=shell)
return self._user_cache
@property
def _groups(self):
"""Returns a list of Group tuples."""
if self._group_cache is not None:
return self._group_cache
self._group_cache = {}
group_contents = osutils.ReadFile(self._group_db_file)
for line in group_contents.splitlines():
pieces = line.split(':')
if len(pieces) != 4:
logging.warning('Ignored invalid line in group file: "%s"', line)
continue
group, password, gid, users = pieces
try:
gid_as_int = int(gid)
except ValueError:
logging.warning('Ignored invalid or gid (%s).', gid)
continue
if group in self._group_cache:
logging.warning('Ignored duplicate group definition for "%s".',
group)
continue
users = users.split(',')
self._group_cache[group] = Group(group=group, password=password,
gid=gid_as_int, users=users)
return self._group_cache
def GetUserEntry(self, username, skip_lock=False):
"""Returns a user's database entry.
Args:
username: name of user to get the entry for.
skip_lock: True iff we should skip getting a lock before reading the
database.
Returns:
database entry as a string.
"""
if skip_lock:
return UserToEntry(self._users[username])
# Clear the user cache to force ourselves to reparse while holding a lock.
self._user_cache = None
with locking.PortableLinkLock(
self._user_db_file + '.lock', max_retry=self._DB_LOCK_RETRIES):
return UserToEntry(self._users[username])
def GetGroupEntry(self, groupname, skip_lock=False):
"""Returns a group's database entry.
Args:
groupname: name of group to get the entry for.
skip_lock: True iff we should skip getting a lock before reading the
database.
Returns:
database entry as a string.
"""
if skip_lock:
return GroupToEntry(self._groups[groupname])
# Clear the group cache to force ourselves to reparse while holding a lock.
self._group_cache = None
with locking.PortableLinkLock(
self._group_db_file + '.lock', max_retry=self._DB_LOCK_RETRIES):
return GroupToEntry(self._groups[groupname])
def UserExists(self, username):
"""Returns True iff a user called |username| exists in the database.
Args:
username: name of a user (e.g. 'root')
Returns:
True iff the given |username| has an entry in /etc/passwd.
"""
return username in self._users
def GroupExists(self, groupname):
"""Returns True iff a group called |groupname| exists in the database.
Args:
groupname: name of a group (e.g. 'root')
Returns:
True iff the given |groupname| has an entry in /etc/group.
"""
return groupname in self._groups
def ResolveUsername(self, username):
"""Resolves a username to a uid.
Args:
username: name of a user (e.g. 'root')
Returns:
The uid of the given username. Raises ValueError on failure.
"""
user = self._users.get(username)
if user:
return user.uid
raise ValueError('Could not resolve unknown user "%s" to uid.' % username)
def ResolveGroupname(self, groupname):
"""Resolves a groupname to a gid.
Args:
groupname: name of a group (e.g. 'wheel')
Returns:
The gid of the given groupname. Raises ValueError on failure.
"""
group = self._groups.get(groupname)
if group:
return group.gid
raise ValueError('Could not resolve unknown group "%s" to gid.' % groupname)
def AddUser(self, user):
"""Atomically add a user to the database.
If a user named |user.user| already exists, this method will simply return.
Args:
user: user_db.User object to add to database.
"""
# Try to avoid grabbing the lock in the common case that a user already
# exists.
if self.UserExists(user.user):
logging.info('Not installing user "%s" because it already existed.',
user.user)
return
# Clear the user cache to force ourselves to reparse.
self._user_cache = None
with locking.PortableLinkLock(self._user_db_file + '.lock',
max_retry=self._DB_LOCK_RETRIES):
# Check that |user| exists under the lock in case we're racing to create
# this user.
if self.UserExists(user.user):
logging.info('Not installing user "%s" because it already existed.',
user.user)
return
self._users[user.user] = user
new_users = sorted(self._users.itervalues(), key=lambda u: u.uid)
contents = '\n'.join([UserToEntry(u) for u in new_users])
osutils.WriteFile(self._user_db_file, contents, atomic=True, sudo=True)
print('Added user "%s" to %s:' % (user.user, self._user_db_file))
print(' - password entry: %s' % user.password)
print(' - id: %d' % user.uid)
print(' - group id: %d' % user.gid)
print(' - gecos: %s' % user.gecos)
print(' - home: %s' % user.home)
print(' - shell: %s' % user.shell)
def AddGroup(self, group):
"""Atomically add a group to the database.
If a group named |group.group| already exists, this method will simply
return.
Args:
group: user_db.Group object to add to database.
"""
# Try to avoid grabbing the lock in the common case that a group already
# exists.
if self.GroupExists(group.group):
logging.info('Not installing group "%s" because it already existed.',
group.group)
return
# Clear the group cache to force ourselves to reparse.
self._group_cache = None
with locking.PortableLinkLock(self._group_db_file + '.lock',
max_retry=self._DB_LOCK_RETRIES):
# Check that |group| exists under the lock in case we're racing to create
# this group.
if self.GroupExists(group.group):
logging.info('Not installing group "%s" because it already existed.',
group.group)
return
self._groups[group.group] = group
new_groups = sorted(self._groups.itervalues(), key=lambda g: g.gid)
contents = '\n'.join([GroupToEntry(g) for g in new_groups])
osutils.WriteFile(self._group_db_file, contents, atomic=True, sudo=True)
print('Added group "%s" to %s:' % (group.group, self._group_db_file))
print(' - group id: %d' % group.gid)
print(' - password entry: %s' % group.password)
print(' - user list: %s' % ','.join(group.users))