Skip to content

Commit 7a945e2

Browse files
committed
Add user company/role management commands
1 parent e04b46f commit 7a945e2

10 files changed

Lines changed: 657 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [2.5.6] - 2026-06-06
11+
12+
### Added
13+
- `user-company:assign --company_id <id> (--user_id <id> | --login <login> | --email <email>) [--role <role>]` - assign a user to a company with an optional company-scoped role
14+
- `user-company:unassign --company_id <id> (--user_id <id> | --login <login> | --email <email>)` - remove a user-company assignment
15+
- `user-role:set (--user_id <id> | --login <login> | --email <email>) --roles <role1,role2,...> [--replace] [--assigned_by <id>]` - assign RBAC roles by role name
16+
17+
### Changed
18+
- Updated command documentation in `README.md`, `doc/multiflexi-cli.rst`, and `debian/multiflexi-cli.1`
19+
- Added command metadata tests in `tests/src/Command/UserAccessCommandsTest.php`
20+
1021
## [2.5.5] - 2026-05-28
1122

1223
### Fixed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ multiflexi-cli <command> [options]
4747
| `job:status --id <id>` | Get job status |
4848
| `company:list` | List companies |
4949
| `company-app:list` | List company–application assignments |
50+
| `user-company:assign --company_id <id> --user_id <id>` | Assign user to company |
51+
| `user-company:unassign --company_id <id> --user_id <id>` | Unassign user from company |
52+
| `user-role:set --user_id <id> --roles <r1,r2>` | Set RBAC roles for user |
5053
| `credential-type:list` | List credential types |
5154
| `credential-type:import-json --file <file>` | Import credential type from JSON |
5255
| `user:list` | List users |
@@ -94,6 +97,14 @@ multiflexi-cli job:status --id 123
9497
multiflexi-cli user:create --login "jsmith" --email "john@example.com"
9598
multiflexi-cli user:list --format json
9699

100+
# User-company assignments
101+
multiflexi-cli user-company:assign --company_id 2 --login "jsmith" --role viewer
102+
multiflexi-cli user-company:unassign --company_id 2 --login "jsmith"
103+
104+
# User RBAC roles
105+
multiflexi-cli user-role:set --login "jsmith" --roles admin,viewer --replace=true
106+
multiflexi-cli user-role:set --login "jsmith" --roles editor --replace=false
107+
97108
# Credential types
98109
multiflexi-cli credential-type:list
99110
multiflexi-cli credential-type:get --uuid "d3d3ae58-d64a-4ab4-afb5-ba439ffc8587"

debian/changelog

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
multiflexi-cli (2.5.6) UNRELEASED; urgency=medium
2+
3+
* Add user-company:assign command
4+
* Add user-company:unassign command
5+
* Add user-role:set command for RBAC role assignment
6+
* Update docs: README, doc/multiflexi-cli.rst, debian/multiflexi-cli.1
7+
* Add tests: UserAccessCommandsTest
8+
9+
-- Vítězslav Dvořák <info@vitexsoftware.cz> Sat, 06 Jun 2026 12:00:00 +0200
10+
111
multiflexi-cli (2.5.5) UNRELEASED; urgency=medium
212

313
* Fix: rename --runtemplate_id to --id on run-template:assign-credential,

debian/multiflexi-cli.1

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,18 @@ Manage users.
8080
Options: --id, --login, --email, --firstname, --lastname, --password, --plaintext, --enabled, --format
8181
.RE
8282
.TP
83+
.B user-company:assign / user-company:unassign
84+
Assign and unassign users to/from companies.
85+
.RS
86+
Options: --company_id, --user_id, --login, --email, --role (assign only), --format
87+
.RE
88+
.TP
89+
.B user-role:set
90+
Set RBAC roles for a user.
91+
.RS
92+
Options: --user_id, --login, --email, --roles, --replace, --assigned_by, --format
93+
.RE
94+
.TP
8395
.B user-erasure:list / user-erasure:create / user-erasure:approve / user-erasure:reject / user-erasure:process / user-erasure:audit / user-erasure:cleanup
8496
Manage GDPR user data erasure requests (Article 17 Right to Erasure).
8597
.RS
@@ -268,6 +280,15 @@ Generate a new token for user ID 2:
268280
Assign application to company:
269281
.B multiflexi-cli company-app:assign --company_id=1 --app_id=5
270282
.TP
283+
Assign user to company:
284+
.B multiflexi-cli user-company:assign --company_id=2 --login=jsmith --role=viewer
285+
.TP
286+
Unassign user from company:
287+
.B multiflexi-cli user-company:unassign --company_id=2 --login=jsmith
288+
.TP
289+
Set RBAC roles for user:
290+
.B multiflexi-cli user-role:set --login=jsmith --roles=admin,viewer --replace=true
291+
.TP
271292
Import credential type from JSON:
272293
.B multiflexi-cli credential-type:import-json --file=credtype.json
273294
.TP

doc/multiflexi-cli.rst

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ The MultiFlexi CLI provides the following main commands:
5050
- **application:\*** - Manage applications (list, get, create, update, delete, import/export/remove JSON, show config)
5151
- **company:\*** - Manage companies and their settings
5252
- **company-app:\*** - Manage company-application relations (list, assign, unassign)
53+
- **user-company:\*** - Assign/unassign users to companies
54+
- **user-role:\*** - Set RBAC roles for users
5355
- **job:\*** - Manage job execution and monitoring
5456
- **run-template:\*** - Manage run templates, scheduling, and credential assignment
5557
- **user:\*** - User account management
@@ -144,6 +146,48 @@ Examples:
144146
multiflexi-cli company-app:assign --company_id=1 --app_uuid=uuid-123 --format=json
145147
multiflexi-cli company-app:unassign --company_id=1 --app_id=2
146148
149+
user-company
150+
------------
151+
152+
Manage user-company assignments (assign, unassign).
153+
154+
Options:
155+
--company_id Company ID (required)
156+
--user_id User ID (required unless --login/--email is used)
157+
--login User login (alternative to --user_id)
158+
--email User email (alternative to --user_id)
159+
--role Assignment role for ``company_user.role`` (assign only, default: ``viewer``)
160+
-f, --format Output format: text or json (default: text)
161+
162+
Examples:
163+
164+
.. code-block:: bash
165+
166+
multiflexi-cli user-company:assign --company_id=2 --user_id=10 --role=viewer
167+
multiflexi-cli user-company:assign --company_id=2 --login=jsmith --role=editor
168+
multiflexi-cli user-company:unassign --company_id=2 --email=john@example.com
169+
170+
user-role
171+
---------
172+
173+
Set RBAC roles for users.
174+
175+
Options:
176+
--user_id User ID (required unless --login/--email is used)
177+
--login User login (alternative to --user_id)
178+
--email User email (alternative to --user_id)
179+
--roles Comma-separated role names (e.g. ``admin,viewer``)
180+
--replace Replace existing assignments (true/false, default: true)
181+
--assigned_by Optional user ID to store as assigner
182+
-f, --format Output format: text or json (default: text)
183+
184+
Examples:
185+
186+
.. code-block:: bash
187+
188+
multiflexi-cli user-role:set --user_id=10 --roles=admin,viewer --replace=true
189+
multiflexi-cli user-role:set --login=jsmith --roles=editor --replace=false --assigned_by=1
190+
147191
credential-type
148192
---------------
149193

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MultiFlexi\Cli\Command\UserCompany;
6+
7+
use MultiFlexi\Cli\Command\MultiFlexiCommand;
8+
use MultiFlexi\Company;
9+
use MultiFlexi\User;
10+
use Symfony\Component\Console\Input\InputInterface;
11+
use Symfony\Component\Console\Input\InputOption;
12+
use Symfony\Component\Console\Output\OutputInterface;
13+
14+
class AssignCommand extends MultiFlexiCommand
15+
{
16+
protected static $defaultName = 'user-company:assign';
17+
18+
protected function configure(): void
19+
{
20+
$this
21+
->setName('user-company:assign')
22+
->setDescription('Assign a user to a company')
23+
->addOption('format', 'f', InputOption::VALUE_OPTIONAL, 'Output format: text or json', 'text')
24+
->addOption('company_id', null, InputOption::VALUE_REQUIRED, 'Company ID')
25+
->addOption('user_id', null, InputOption::VALUE_REQUIRED, 'User ID')
26+
->addOption('login', null, InputOption::VALUE_REQUIRED, 'User login (alternative to --user_id)')
27+
->addOption('email', null, InputOption::VALUE_REQUIRED, 'User email (alternative to --user_id)')
28+
->addOption('role', null, InputOption::VALUE_OPTIONAL, 'Assignment role in company_user table', 'viewer');
29+
}
30+
31+
protected function execute(InputInterface $input, OutputInterface $output): int
32+
{
33+
$format = strtolower((string) $input->getOption('format'));
34+
$companyId = (int) $input->getOption('company_id');
35+
$role = (string) $input->getOption('role');
36+
37+
if ($companyId <= 0) {
38+
$msg = 'Missing or invalid --company_id';
39+
$format === 'json' ? $this->jsonError($output, $msg) : $output->writeln("<error>{$msg}</error>");
40+
41+
return self::FAILURE;
42+
}
43+
44+
$company = new Company($companyId);
45+
46+
if (empty($company->getData())) {
47+
$msg = "Company #{$companyId} not found";
48+
$format === 'json' ? $this->jsonError($output, $msg) : $output->writeln("<error>{$msg}</error>");
49+
50+
return self::FAILURE;
51+
}
52+
53+
$userId = $this->resolveUserId($input);
54+
55+
if ($userId <= 0) {
56+
$msg = 'Provide --user_id or --login or --email';
57+
$format === 'json' ? $this->jsonError($output, $msg) : $output->writeln("<error>{$msg}</error>");
58+
59+
return self::FAILURE;
60+
}
61+
62+
$user = new User($userId);
63+
64+
if (empty($user->getData())) {
65+
$msg = "User #{$userId} not found";
66+
$format === 'json' ? $this->jsonError($output, $msg) : $output->writeln("<error>{$msg}</error>");
67+
68+
return self::FAILURE;
69+
}
70+
71+
$pdo = $this->connectPdo();
72+
73+
if (!$this->tableExists($pdo, 'company_user')) {
74+
$msg = 'Table company_user does not exist. Run database migrations first.';
75+
$format === 'json' ? $this->jsonError($output, $msg) : $output->writeln("<error>{$msg}</error>");
76+
77+
return self::FAILURE;
78+
}
79+
80+
$sql = 'INSERT INTO company_user (company_id, user_id, role) VALUES (?, ?, ?) '
81+
.'ON DUPLICATE KEY UPDATE role = VALUES(role)';
82+
$ok = $pdo->prepare($sql)->execute([$companyId, $userId, $role]);
83+
84+
if (!$ok) {
85+
$msg = 'Failed to assign user to company';
86+
$format === 'json' ? $this->jsonError($output, $msg) : $output->writeln("<error>{$msg}</error>");
87+
88+
return self::FAILURE;
89+
}
90+
91+
if ($format === 'json') {
92+
$this->jsonSuccess($output, 'User assigned to company', [
93+
'company_id' => $companyId,
94+
'user_id' => $userId,
95+
'role' => $role,
96+
]);
97+
} else {
98+
$output->writeln("User #{$userId} assigned to company #{$companyId} with role '{$role}'");
99+
}
100+
101+
return self::SUCCESS;
102+
}
103+
104+
private function resolveUserId(InputInterface $input): int
105+
{
106+
$userId = (int) $input->getOption('user_id');
107+
108+
if ($userId > 0) {
109+
return $userId;
110+
}
111+
112+
$login = trim((string) $input->getOption('login'));
113+
$email = trim((string) $input->getOption('email'));
114+
115+
if ($login !== '') {
116+
$found = (new User())->listingQuery()->where(['login' => $login])->fetch();
117+
118+
return $found ? (int) $found['id'] : 0;
119+
}
120+
121+
if ($email !== '') {
122+
$found = (new User())->listingQuery()->where(['email' => $email])->fetch();
123+
124+
return $found ? (int) $found['id'] : 0;
125+
}
126+
127+
return 0;
128+
}
129+
130+
private function connectPdo(): \PDO
131+
{
132+
return new \PDO(
133+
\Ease\Shared::cfg('DB_CONNECTION').':host='.\Ease\Shared::cfg('DB_HOST').';port='.(string) \Ease\Shared::cfg('DB_PORT', 3306).';dbname='.\Ease\Shared::cfg('DB_DATABASE').';charset=utf8mb4',
134+
\Ease\Shared::cfg('DB_USERNAME'),
135+
\Ease\Shared::cfg('DB_PASSWORD'),
136+
[\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION],
137+
);
138+
}
139+
140+
private function tableExists(\PDO $pdo, string $table): bool
141+
{
142+
$stmt = $pdo->prepare('SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = ? AND table_name = ?');
143+
$stmt->execute([\Ease\Shared::cfg('DB_DATABASE'), $table]);
144+
145+
return (int) $stmt->fetchColumn() > 0;
146+
}
147+
}

0 commit comments

Comments
 (0)