Skip to content

Commit

Permalink
add: user management on Core
Browse files Browse the repository at this point in the history
  • Loading branch information
st3v3nmw committed Feb 5, 2024
1 parent 340e8fc commit b4bd000
Show file tree
Hide file tree
Showing 7 changed files with 379 additions and 17 deletions.
124 changes: 122 additions & 2 deletions landscape/client/manager/tests/test_usermanager.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
from unittest.mock import Mock
from unittest.mock import patch

from landscape.client.manager.plugin import FAILED
from landscape.client.manager.plugin import SUCCEEDED
Expand All @@ -9,6 +10,7 @@
from landscape.client.tests.helpers import LandscapeTest
from landscape.client.tests.helpers import ManagerHelper
from landscape.client.user.provider import UserManagementError
from landscape.client.user.tests.helpers import FakeSnapdUserManagement
from landscape.client.user.tests.helpers import FakeUserManagement
from landscape.client.user.tests.helpers import FakeUserProvider
from landscape.lib.persist import Persist
Expand Down Expand Up @@ -36,14 +38,18 @@ def tearDown(self):
for plugin in self.plugins:
plugin.stop()

def setup_environment(self, users, groups, shadow_file):
def setup_environment(self, users, groups, shadow_file, is_core=False):
provider = FakeUserProvider(
users=users,
groups=groups,
shadow_file=shadow_file,
)
user_monitor = UserMonitor(provider=provider)
management = FakeUserManagement(provider=provider)

if is_core:
management = FakeSnapdUserManagement(provider=provider)
else:
management = FakeUserManagement(provider=provider)
user_manager = UserManager(
management=management,
shadow_file=shadow_file,
Expand Down Expand Up @@ -378,6 +384,64 @@ def handle_callback(result):
result.addCallback(handle_callback)
return result

@patch("landscape.client.manager.usermanager.IS_CORE", "1")
def test_add_user_event_on_core(self):
"""
When an C{add-user} event is received the user should be
added. Two messages should be generated: a C{users} message
with details about the change and an C{operation-result} with
details of the outcome of the operation.
"""

def handle_callback(result):
messages = self.broker_service.message_store.get_pending_messages()
self.assertMessages(
messages,
[
{
"type": "operation-result",
"status": SUCCEEDED,
"operation-id": 123,
"timestamp": 0,
"result-text": "add_user succeeded",
},
{
"timestamp": 0,
"type": "users",
"operation-id": 123,
"create-users": [
{
"home-phone": None,
"username": "john-doe",
"uid": 1000,
"enabled": True,
"location": None,
"work-phone": None,
"name": "[email protected]",
"primary-gid": 1000,
},
],
},
],
)

shadow_file = self.makeFile("""st3v3nmw:*:19758:0:99999:7:::""")
self.setup_environment([], [], shadow_file, is_core=True)

result = self.manager.dispatch_message(
{
"type": "add-user",
"username": "john-doe",
"email": "[email protected]",
"sudoer": False,
"force-managed": True,
"operation-id": 123,
},
)

result.addCallback(handle_callback)
return result

def test_edit_user_event(self):
"""
When a C{edit-user} message is received the user should be
Expand Down Expand Up @@ -834,6 +898,62 @@ def handle_callback(result):
result.addCallback(handle_callback)
return result

@patch("landscape.client.manager.usermanager.IS_CORE", "1")
def test_remove_user_event_on_core(self):
"""
When a C{remove-user} event is received, the user should be removed.
Two messages should be generated: a C{users} message with details
about the change and an C{operation-result} with details of the
outcome of the operation.
"""

def handle_callback(result):
messages = self.broker_service.message_store.get_pending_messages()
self.assertEqual(len(messages), 3)
# Ignore the message created by plugin.run.
self.assertMessages(
[messages[2], messages[1]],
[
{
"timestamp": 0,
"delete-users": ["john-doe"],
"type": "users",
"operation-id": 39,
},
{
"type": "operation-result",
"status": SUCCEEDED,
"operation-id": 39,
"timestamp": 0,
"result-text": "remove_user succeeded",
},
],
)

users = [
(
"john-doe",
"x",
1000,
1000,
"[email protected],BtrGAhK,,",
"/home/user",
"/bin/zsh",
),
]
shadow_file = self.makeFile("""st3v3nmw:*:19758:0:99999:7:::""")
self.setup_environment(users, [], shadow_file, is_core=True)
result = self.manager.dispatch_message(
{
"username": "john-doe",
"delete-home": True,
"type": "remove-user",
"operation-id": 39,
},
)
result.addCallback(handle_callback)
return result

def test_lock_user_event(self):
"""
When a C{lock-user} event is received the user should be
Expand Down
51 changes: 38 additions & 13 deletions landscape/client/manager/usermanager.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import logging

from landscape.client import IS_CORE
from landscape.client.amp import ComponentConnector
from landscape.client.amp import ComponentPublisher
from landscape.client.amp import remote
from landscape.client.manager.plugin import ManagerPlugin
from landscape.client.monitor.usermonitor import RemoteUserMonitorConnector
from landscape.client.user.management import SnapdUserManagement
from landscape.client.user.management import UserManagement


Expand All @@ -13,20 +15,30 @@ class UserManager(ManagerPlugin):
name = "usermanager"

def __init__(self, management=None, shadow_file="/etc/shadow"):
self._management = management or UserManagement()
if IS_CORE:
management = management or SnapdUserManagement()
shadow_file = shadow_file or "/var/lib/extrausers/shadow"
self._message_types = {
"add-user": self._add_user_core,
"remove-user": self._remove_user_core,
}
else:
management = management or UserManagement()
self._message_types = {
"add-user": self._add_user,
"edit-user": self._edit_user,
"lock-user": self._lock_user,
"unlock-user": self._unlock_user,
"remove-user": self._remove_user,
"add-group": self._add_group,
"edit-group": self._edit_group,
"remove-group": self._remove_group,
"add-group-member": self._add_group_member,
"remove-group-member": self._remove_group_member,
}

self._management = management
self._shadow_file = shadow_file
self._message_types = {
"add-user": self._add_user,
"edit-user": self._edit_user,
"lock-user": self._lock_user,
"unlock-user": self._unlock_user,
"remove-user": self._remove_user,
"add-group": self._add_group,
"edit-group": self._edit_group,
"remove-group": self._remove_group,
"add-group-member": self._add_group_member,
"remove-group-member": self._remove_group_member,
}
self._publisher = None

def register(self, registry):
Expand Down Expand Up @@ -118,6 +130,15 @@ def _add_user(self, message):
message["home-number"],
)

def _add_user_core(self, message):
"""Run an C{add-user} operation on Core."""
return self._management.add_user(
message["username"],
message["email"],
message["sudoer"],
message["force-managed"],
)

def _edit_user(self, message):
"""Run an C{edit-user} operation."""
return self._management.set_user_details(
Expand Down Expand Up @@ -145,6 +166,10 @@ def _remove_user(self, message):
message["delete-home"],
)

def _remove_user_core(self, message):
"""Run a C{remove-user} operation on Core."""
return self._management.remove_user(message["username"])

def _add_group(self, message):
"""Run an C{add-group} operation."""
return self._management.add_group(message["groupname"])
Expand Down
45 changes: 45 additions & 0 deletions landscape/client/monitor/tests/test_usermonitor.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
from unittest.mock import ANY
from unittest.mock import Mock
from unittest.mock import patch

from twisted.internet.defer import fail

Expand Down Expand Up @@ -208,6 +209,50 @@ def test_new_message_after_resynchronize_event(self):
],
)

@patch("landscape.client.manager.usermanager.IS_CORE", "1")
def test_new_message_after_resynchronize_event_on_core(self):
"""
When a 'resynchronize' reactor event is fired, a new session is
created and the UserMonitor creates a new message.
"""
self.provider.users = [
(
"john-doe",
"x",
1000,
1000,
"[email protected]",
"/home/user",
"/bin/zsh",
),
]
self.broker_service.message_store.set_accepted_types(["users"])
plugin = UserMonitor(self.provider)
self.monitor.add(plugin)
plugin.client.broker.message_store.drop_session_ids()
deferred = self.reactor.fire("resynchronize")[0]
self.successResultOf(deferred)
self.assertMessages(
self.broker_service.message_store.get_pending_messages(),
[
{
"create-users": [
{
"enabled": True,
"home-phone": None,
"location": None,
"name": "[email protected]",
"primary-gid": 1000,
"uid": 1000,
"username": "john-doe",
"work-phone": None,
},
],
"type": "users",
},
],
)

def test_wb_resynchronize_event_with_global_scope(self):
"""
When a C{resynchronize} event, with global scope, occurs we act exactly
Expand Down
11 changes: 9 additions & 2 deletions landscape/client/monitor/usermonitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from twisted.internet.defer import maybeDeferred

from landscape.client import IS_CORE
from landscape.client.amp import ComponentConnector
from landscape.client.amp import ComponentPublisher
from landscape.client.amp import remote
Expand All @@ -27,8 +28,14 @@ class UserMonitor(MonitorPlugin):
name = "usermonitor"

def __init__(self, provider=None):
if provider is None:
provider = UserProvider()
if IS_CORE:
provider = provider or UserProvider(

Check warning on line 32 in landscape/client/monitor/usermonitor.py

View check run for this annotation

Codecov / codecov/patch

landscape/client/monitor/usermonitor.py#L32

Added line #L32 was not covered by tests
passwd_file="/var/lib/extrausers/passwd",
group_file="/var/lib/extrausers/group",
)
else:
provider = provider or UserProvider()

self._provider = provider
self._publisher = None

Expand Down
38 changes: 38 additions & 0 deletions landscape/client/user/management.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
# API, with thorough usage of exceptions and such, instead of pipes to
# subprocesses. liboobs (i.e. System Tools) is a possibility, and has
# documentation now in the 2.17 series, but is not wrapped to Python.
import json
import logging
import subprocess

from landscape.client import snap_http
from landscape.client.snap_http import SnapdHttpException
from landscape.client.user.provider import UserManagementError
from landscape.client.user.provider import UserProvider

Expand Down Expand Up @@ -284,3 +287,38 @@ def call_popen(self, args):
output = popen.stdout.read()
result = popen.wait()
return result, output


class SnapdUserManagement:
"""Manage users via the Snapd API."""

def __init__(self, provider=None):
self._provider = provider or UserProvider(
passwd_file="/var/lib/extrausers/passwd",
group_file="/var/lib/extrausers/group",
)

def add_user(self, username, email, sudoer=False, force_managed=False):
"""Add a user via the Snapd API."""
try:
response = snap_http.add_user(
username,
email,
sudoer=sudoer,
force_managed=force_managed,
)
except SnapdHttpException as e:
result = json.loads(e.args[0])["result"]
raise UserManagementError(result)

return response.result

def remove_user(self, username):
"""Remove a user via the Snapd API."""
try:
response = snap_http.remove_user(username)
except SnapdHttpException as e:
result = json.loads(e.args[0])["result"]
raise UserManagementError(result)

return response.result
Loading

0 comments on commit b4bd000

Please sign in to comment.