Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- **Group management over HTTP and in the CLI.** `POST /user/groups`, `DELETE /user/groups/{name}`
and `POST`/`DELETE` `/user/groups/{name}/subgroups/{sub}` (all `Admin-User`) expose the group
operations `GroupRepository` already had; the CLI gains `group_add`, `group_remove`,
`group_add_subgroup` and `group_remove_subgroup`. Virtual groups stay code-defined and are
rejected with 400, and a group that a user still belongs to cannot be deleted (409).
- **Migration hook for shared memory segments.** `lintel::shm::SegmentLayoutPolicy` lets the
application decide what happens when a segment was written with another object layout: refuse
(default), recreate, or run a registered migrator that sees the untouched segment file and can
Expand Down Expand Up @@ -297,6 +302,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Hypodermic was an unmaintained dependency; `lintel::di` replaces it.

### Fixed
- **Deleting a group left dangling sub-group relations.** `GroupRepository::deleteOf()` removed the
`groups` row but not the `group_groups_relation` rows pointing at it, so the next `initGroups()`
dereferenced the gone group (`m_groupMap.at`) and aborted the start. It now removes the relations
in both directions and drops the group from the member lists of its parents.
- **The SHM message queue was not an IPC mechanism.** Every process built its own ring in the
segment, so `MessageQueueShmCore` only worked inside one process. A fork-based test now proves
that a child process finds the queue by name and that a message crosses the boundary.
Expand Down
26 changes: 26 additions & 0 deletions src/features/base/repositories/GroupRepository.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -175,12 +175,38 @@ void GroupRepository::deleteOf(const Group &group)
"group_name = ?",
builder);
if (result.getSize() > 0) { throw std::runtime_error("cannot remove group which is in usage of an user"); }
// A group can be the sub-group of another one. Those rows point at it from
// both directions, and leaving them behind makes the next initGroups()
// dereference a group that no longer exists (m_groupMap.at).
db::ParameterBuilder relationBuilder(m_connectionEntry);
relationBuilder.add(group.getGroupName()).add(group.getGroupName());
statement.execute("delete from group_groups_relation where group_name = ? or base_group_name = ?", relationBuilder);
statement.execute("delete from groups where name = ?", builder);
m_groupMap.erase(group.getGroupName());
m_groups.erase(
std::remove_if(
m_groups.begin(), m_groups.end(), [&](const Group &g) { return g.getGroupName() == group.getGroupName(); }),
m_groups.end());
// drop it from the member lists of the groups that contained it, so that
// allOf() does not keep reporting a group that is gone
const auto withoutDeleted = [&group](const std::vector<Group> &members) {
std::vector<Group> kept;
kept.reserve(members.size());
for (const auto &member : members) {
if (member.getGroupName() != group.getGroupName()) { kept.push_back(member); }
}
return kept;
};
for (auto &[name, candidate] : m_groupMap) {
const std::vector<Group> members = withoutDeleted(candidate.getGroups());
if (members.size() == candidate.getGroups().size()) { continue; }
candidate = Group(candidate.getGroupName(), members, candidate.isVirtual());
}
for (auto &candidate : m_groups) {
const std::vector<Group> members = withoutDeleted(candidate.getGroups());
if (members.size() == candidate.getGroups().size()) { continue; }
candidate = Group(candidate.getGroupName(), members, candidate.isVirtual());
}
}

void GroupRepository::addGroupOf(const Group &group, const Group &add)
Expand Down
71 changes: 66 additions & 5 deletions src/features/cli/components/UserManagementCliComponent.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,25 @@ UserManagementCliComponent::UserManagementCliComponent(std::shared_ptr<UserApi>
.addArgument(
{ "--is-virtual", "-v" }, &m_isVirtualGroup, "Limits search result to virtual or non virtual groups"),
AllGroups);
m_commandParser.addCommand(
Command("group_add", "Creates a group!")
.addArgument({ "--group", "-g" }, &m_groupName, "Name of the group which should be created", false)
.addArgument({ "--virtual", "-v" }, &m_isVirtualGroup, "Creates a virtual group", true),
AddGroup);
m_commandParser.addCommand(
Command("group_remove", "Deletes a group!")
.addArgument({ "--group", "-g" }, &m_groupName, "Name of the group which should be deleted", false),
RemoveGroup);
m_commandParser.addCommand(
Command("group_add_subgroup", "Adds a virtual group as member of a collecting group!")
.addArgument({ "--group", "-g" }, &m_groupName, "Name of the collecting group", false)
.addArgument({ "--subgroup", "-s" }, &m_subGroupName, "Name of the virtual group to add", false),
AddSubGroup);
m_commandParser.addCommand(
Command("group_remove_subgroup", "Removes a virtual group from a collecting group!")
.addArgument({ "--group", "-g" }, &m_groupName, "Name of the collecting group", false)
.addArgument({ "--subgroup", "-s" }, &m_subGroupName, "Name of the virtual group to remove", false),
RemoveSubGroup);
m_commandParser.addCommand(
Command("show_users", "Show all available users!")
.addArgument(
Expand Down Expand Up @@ -60,10 +79,12 @@ UserManagementCliComponent::UserManagementCliComponent(std::shared_ptr<UserApi>
* Dispatches an entered CLI command to the UserApi.
*
* Flow: the parser splits the input into a command and its parameters, then a
* switch over the eight commands executes the matching UserApi operation and
* prints the result. All filters (group name, virtual group) are taken from the
* previously set session context, not from the parameters - the CLI session
* carries them.
* switch over the registered commands executes the matching UserApi operation
* and prints the result. Filter flags (group name, virtual group) of the
* listing commands are taken from the parsed parameters of that command, not
* from previously executed ones - the parser resets them for every command.
* The group management commands take the target names from the parameters as
* well.
*
* Every call is wrapped in a try/catch: a failed UserApi operation should not
* end the menu loop but show up as an error message. The handler therefore
Expand All @@ -80,7 +101,7 @@ void UserManagementCliComponent::onCommand(const UserDto &userDto,
try {
Commands command = m_commandParser.parse(input, parameters);
switch (command) {
// groups: filters come from the session context, not from the parameters.
// groups: the four listing filter combinations of name and virtual flag.
case AllGroups:
if (!m_groupName.has_value() && !m_isVirtualGroup.has_value()) {
printGroups(m_userApi->allOf());
Expand All @@ -92,6 +113,46 @@ void UserManagementCliComponent::onCommand(const UserDto &userDto,
printGroups(m_userApi->allOf(m_groupName.value(), m_isVirtualGroup.value()));
}
break;
// groups: create and delete take the name from the parameters; the
// virtual flag is only meaningful for the creation.
case AddGroup: {
if (!m_groupName.has_value()) {
std::cerr << "ERROR: please set the group name\n";
break;
}
GroupDto groupDto(Group(m_groupName.value(), {}, m_isVirtualGroup.value_or(false)));
if (!m_userApi->createGroupOf(groupDto)) { std::cerr << "ERROR: failed to create group\n"; }
break;
}
case RemoveGroup: {
if (!m_groupName.has_value()) {
std::cerr << "ERROR: please set the group name\n";
break;
}
if (!m_userApi->deleteGroupOf(m_groupName.value())) { std::cerr << "ERROR: failed to delete group\n"; }
break;
}
// subgroups: a collecting group plus the virtual group to add or remove.
case AddSubGroup: {
if (!m_groupName.has_value() || !m_subGroupName.has_value()) {
std::cerr << "ERROR: please set the group and subgroup name\n";
break;
}
if (!m_userApi->addSubGroupOf(m_groupName.value(), m_subGroupName.value())) {
std::cerr << "ERROR: failed to add subgroup\n";
}
break;
}
case RemoveSubGroup: {
if (!m_groupName.has_value() || !m_subGroupName.has_value()) {
std::cerr << "ERROR: please set the group and subgroup name\n";
break;
}
if (!m_userApi->removeSubGroupOf(m_groupName.value(), m_subGroupName.value())) {
std::cerr << "ERROR: failed to remove subgroup\n";
}
break;
}
// users: same, the same four filter combinations.
case AllUsers: {
if (m_userName.has_value()) {
Expand Down
73 changes: 73 additions & 0 deletions src/features/http/controllers/UserApi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,79 @@ std::vector<GroupDto> UserApi::allOf(bool isVirtualGroup)
return groupsDto.getGroups();
}

bool UserApi::createGroupOf(const GroupDto &groupDto)
{
std::string body;
try {
body = groupDto.JsonSerializable::serialize();
} catch (const std::exception &exception) {
LOG_ERROR("failed to create group {}", groupDto.getGroupName());
return false;
}
httplib::Result result = m_client->post("/user/groups", body, "application/json");
if (!HttpClientHelper::hasResponse(result)) { return false; }
HttpStatusCodes status(result->status);
switch (status) {
case HttpStatusCodes::Unauthorized:
throw HttpUnauthorizedException();
case HttpStatusCodes::OK:
return true;
default:
LOG_ERROR("failed to create group {} ({})", groupDto.getGroupName(), status.getCode());
return false;
}
}

bool UserApi::deleteGroupOf(const std::string &groupName)
{
httplib::Result result = m_client->deletes("/user/groups/" + groupName, "", "application/json");
if (!HttpClientHelper::hasResponse(result)) { return false; }
HttpStatusCodes status(result->status);
switch (status) {
case HttpStatusCodes::Unauthorized:
throw HttpUnauthorizedException();
case HttpStatusCodes::OK:
return true;
default:
LOG_ERROR("failed to delete group {} ({})", groupName, status.getCode());
return false;
}
}

bool UserApi::addSubGroupOf(const std::string &groupName, const std::string &subGroupName)
{
httplib::Result result =
m_client->post("/user/groups/" + groupName + "/subgroups/" + subGroupName, "", "application/json");
if (!HttpClientHelper::hasResponse(result)) { return false; }
HttpStatusCodes status(result->status);
switch (status) {
case HttpStatusCodes::Unauthorized:
throw HttpUnauthorizedException();
case HttpStatusCodes::OK:
return true;
default:
LOG_ERROR("failed to add group {} to {} ({})", subGroupName, groupName, status.getCode());
return false;
}
}

bool UserApi::removeSubGroupOf(const std::string &groupName, const std::string &subGroupName)
{
httplib::Result result =
m_client->deletes("/user/groups/" + groupName + "/subgroups/" + subGroupName, "", "application/json");
if (!HttpClientHelper::hasResponse(result)) { return false; }
HttpStatusCodes status(result->status);
switch (status) {
case HttpStatusCodes::Unauthorized:
throw HttpUnauthorizedException();
case HttpStatusCodes::OK:
return true;
default:
LOG_ERROR("failed to remove group {} from {} ({})", subGroupName, groupName, status.getCode());
return false;
}
}

void UserApi::createOf(const UserDto &userDto)
{
std::string body;
Expand Down
Loading
Loading