diff --git a/app/Http/Controllers/Api/OAuth2/OAuth2UserApiController.php b/app/Http/Controllers/Api/OAuth2/OAuth2UserApiController.php index 2d7fb053..f15a0290 100644 --- a/app/Http/Controllers/Api/OAuth2/OAuth2UserApiController.php +++ b/app/Http/Controllers/Api/OAuth2/OAuth2UserApiController.php @@ -13,11 +13,16 @@ **/ use App\Http\Controllers\GetAllTrait; +use App\Http\Controllers\Traits\RequestProcessor; +use App\Http\Controllers\UserGroupsValidationRulesFactory; use App\Http\Controllers\UserValidationRulesFactory; +use App\Http\Exceptions\HTTP403ForbiddenException; use App\Http\Utils\HTMLCleaner; use App\ModelSerializers\SerializerRegistry; use Auth\Repositories\IUserRepository; +use Illuminate\Http\JsonResponse; use Illuminate\Http\Request as LaravelRequest; +use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Request; use Illuminate\Support\Facades\Log; @@ -27,6 +32,7 @@ use models\exceptions\ValidationException; use OAuth2\Builders\IdTokenBuilder; use OAuth2\IResourceServerContext; +use OAuth2\Models\IClient; use OAuth2\Repositories\IClientRepository; use OAuth2\ResourceServer\IUserService; use Utils\Http\HttpContentType; @@ -41,6 +47,8 @@ final class OAuth2UserApiController extends OAuth2ProtectedController { use GetAllTrait; + use RequestProcessor; + protected function getAllSerializerType(): string { return SerializerRegistry::SerializerType_Private; @@ -324,4 +332,28 @@ public function get($id) } } + /** + * @param $user_id + * @return JsonResponse|mixed + */ + public function updateUserGroups($user_id): mixed + { + return $this->processRequest(function() use($user_id) { + if(!Request::isJson()) return $this->error400(); + + $payload = Request::json()->all(); + // Creates a Validator instance and validates the data. + $validation = Validator::make($payload, UserGroupsValidationRulesFactory::build($payload)); + if ($validation->fails()) { + $ex = new ValidationException(); + throw $ex->setMessages($validation->messages()->toArray()); + } + $user_groups_payload = [ + "groups" => $payload["groups"], + ]; + $this->openid_user_service->update(intval($user_id), $user_groups_payload); + return $this->updated(); + }); + } + } \ No newline at end of file diff --git a/app/Http/Controllers/Factories/UserGroupsValidationRulesFactory.php b/app/Http/Controllers/Factories/UserGroupsValidationRulesFactory.php new file mode 100644 index 00000000..c1ba0e51 --- /dev/null +++ b/app/Http/Controllers/Factories/UserGroupsValidationRulesFactory.php @@ -0,0 +1,39 @@ + 'sometimes|int_array', + ]; + } + + return [ + 'groups' => 'required|int_array', + ]; + } +} \ No newline at end of file diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index d6997427..6876b5f0 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -76,5 +76,6 @@ class Kernel extends HttpKernel 'openstackid.currentuser.serveradmin.json' => \App\Http\Middleware\CurrentUserIsOpenIdServerAdminJson::class, 'oauth2.currentuser.allow.client.edition' => \App\Http\Middleware\CurrentUserCanEditOAuth2Client::class, 'oauth2.currentuser.owns.client' => \App\Http\Middleware\CurrentUserOwnsOAuth2Client::class, + 'service.account' => \App\Http\Middleware\EnsureServiceAccount::class, ]; } diff --git a/app/Http/Middleware/EnsureServiceAccount.php b/app/Http/Middleware/EnsureServiceAccount.php new file mode 100644 index 00000000..64e1c00f --- /dev/null +++ b/app/Http/Middleware/EnsureServiceAccount.php @@ -0,0 +1,50 @@ +context = $context; + } + + /** + * @param $request + * @param Closure $next + * @return \Illuminate\Http\JsonResponse|mixed + */ + public function handle($request, Closure $next) + { + $application_type = $this->context->getApplicationType(); + if ($application_type != IResourceServerContext::ApplicationType_Service) { + return Response::json(['error' => 'Only service accounts are allowed.'], 403); + } + return $next($request); + } +} diff --git a/app/libs/OAuth2/IUserScopes.php b/app/libs/OAuth2/IUserScopes.php index d8975d22..d4241225 100644 --- a/app/libs/OAuth2/IUserScopes.php +++ b/app/libs/OAuth2/IUserScopes.php @@ -29,4 +29,5 @@ interface IUserScopes const MeRead = 'me/read'; const MeWrite = 'me/write'; const Write = 'users/write'; + const UserGroupWrite = 'users/groups/write'; } \ No newline at end of file diff --git a/app/libs/OpenId/Services/IUserService.php b/app/libs/OpenId/Services/IUserService.php index cde5a325..859ab167 100644 --- a/app/libs/OpenId/Services/IUserService.php +++ b/app/libs/OpenId/Services/IUserService.php @@ -16,6 +16,8 @@ use Illuminate\Http\UploadedFile; use models\exceptions\EntityNotFoundException; use models\exceptions\ValidationException; +use models\utils\IEntity; + /** * Interface IUserService * @package OpenId\Services @@ -72,6 +74,15 @@ public function saveProfileInfo($user_id, $show_pic, $show_full_name, $show_emai */ public function updateProfilePhoto($user_id, UploadedFile $file, $max_file_size = 10485760):User; + /** + * @param int $id + * @param array $payload + * @return IEntity + * @throws ValidationException + * @throws EntityNotFoundException + */ + public function update(int $id, array $payload): IEntity; + /** * @param string $action * @param int $user_id diff --git a/composer.json b/composer.json index 89dfd1b9..9c015091 100644 --- a/composer.json +++ b/composer.json @@ -89,6 +89,7 @@ "Database\\Seeders\\": "database/seeders/" }, "files": [ + "app/Utils/helpers.php", "app/libs/Utils/Html/HtmlHelpers.php" ] }, diff --git a/database/migrations/Version20250805084926.php b/database/migrations/Version20250805084926.php new file mode 100644 index 00000000..2c2b5127 --- /dev/null +++ b/database/migrations/Version20250805084926.php @@ -0,0 +1,61 @@ + IUserScopes::UserGroupWrite, + 'short_description' => 'Allows associate Users to Groups.', + 'description' => 'Allows associate Users to Groups.', + 'system' => false, + 'default' => false, + 'groups' => false, + ] + ], 'users'); + + SeedUtils::seedApiEndpoints('users', [ + [ + 'name' => 'add-user-to-groups', + 'active' => true, + 'route' => '/api/v1/users/{id}/groups', + 'http_method' => 'PUT', + 'scopes' => [ + IUserScopes::UserGroupWrite + ], + ], + ]); + } + + /** + * @param Schema $schema + */ + public function down(Schema $schema):void + { + + } +} diff --git a/database/seeds/ApiEndpointSeeder.php b/database/seeds/ApiEndpointSeeder.php index 87d00e25..51050a2e 100644 --- a/database/seeds/ApiEndpointSeeder.php +++ b/database/seeds/ApiEndpointSeeder.php @@ -11,6 +11,8 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ + +use App\libs\OAuth2\IUserScopes; use Illuminate\Database\Seeder; use Illuminate\Support\Facades\DB; /** @@ -119,6 +121,15 @@ private function seedUsersEndpoints() \App\libs\OAuth2\IUserScopes::MeWrite ], ], + [ + 'name' => 'add-user-to-groups', + 'active' => true, + 'route' => '/api/v1/users/{id}/groups', + 'http_method' => 'PUT', + 'scopes' => [ + \App\libs\OAuth2\IUserScopes::UserGroupWrite + ], + ], ] ); } diff --git a/database/seeds/ApiScopeSeeder.php b/database/seeds/ApiScopeSeeder.php index a81c8973..a324e7cb 100644 --- a/database/seeds/ApiScopeSeeder.php +++ b/database/seeds/ApiScopeSeeder.php @@ -91,6 +91,14 @@ private function seedUsersScopes(){ 'system' => false, 'default' => false, 'groups' => false, + ], + [ + 'name' => IUserScopes::UserGroupWrite, + 'short_description' => 'Allows associate Users to Groups', + 'description' => 'Allows associate Users to Groups', + 'system' => false, + 'default' => false, + 'groups' => false, ] ], 'users'); diff --git a/routes/api.php b/routes/api.php index 4df79fda..6c2a852f 100644 --- a/routes/api.php +++ b/routes/api.php @@ -31,6 +31,7 @@ Route::group(['prefix' => '{id}'], function () { Route::get('', 'OAuth2UserApiController@get'); Route::put('', 'OAuth2UserApiController@update'); + Route::put('groups', ['middleware' => 'service.account', 'uses' => 'OAuth2UserApiController@updateUserGroups']); }); Route::group(['prefix' => 'me'], function () { diff --git a/tests/OAuth2ProtectedServiceAppApiTestCase.php b/tests/OAuth2ProtectedServiceAppApiTestCase.php new file mode 100644 index 00000000..a2d540d3 --- /dev/null +++ b/tests/OAuth2ProtectedServiceAppApiTestCase.php @@ -0,0 +1,86 @@ +getScopes(); + + $client_id = '11z87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x.openstack.client'; + $client_secret = '11c/6Y5N7kOtGKhg11c/6Y5N7kOtGKhg11c/6Y5N7kOtGKhg11c/6Y5N7kOtGKhg'; + + $params = [ + 'client_id' => $client_id, + 'redirect_uri' => 'https://www.test.com/oauth2', + 'response_type' => OAuth2Protocol::OAuth2Protocol_ResponseType_Code, + 'scope' => implode(' ', $scope), + OAuth2Protocol::OAuth2Protocol_AccessType => OAuth2Protocol::OAuth2Protocol_AccessType_Offline, + ]; + + Session::put("openid.authorization.response", IAuthService::AuthorizationResponse_AllowOnce); + + $response = $this->action("POST", "OAuth2\OAuth2ProviderController@auth", $params,); + + $url = $response->getTargetUrl(); + + $comps = @parse_url($url); + $query = $comps['query']; + $output = []; + parse_str($query, $output); + + $params = [ + 'code' => $output['code'], + 'redirect_uri' => 'https://www.test.com/oauth2', + 'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_AuthCode, + ]; + + $response = $this->action + ( + "POST", + "OAuth2\OAuth2ProviderController@token", + $params, + [], + [], + [], + array("HTTP_Authorization" => " Basic " . base64_encode($client_id . ':' . $client_secret)) + ); + + $this->assertResponseStatus(200); + + $content = $response->getContent(); + $response = json_decode($content); + + $this->access_token_service_app_type = $response->access_token; + } +} \ No newline at end of file diff --git a/tests/OAuth2UserServiceApiTest.php b/tests/OAuth2UserServiceApiTest.php index 26e1c567..828a3e65 100644 --- a/tests/OAuth2UserServiceApiTest.php +++ b/tests/OAuth2UserServiceApiTest.php @@ -12,11 +12,16 @@ * limitations under the License. **/ use App\libs\OAuth2\IUserScopes; +use Auth\Group; +use Auth\User; +use LaravelDoctrine\ORM\Facades\EntityManager; use OAuth2\ResourceServer\IUserService; +use OAuth2ProtectedServiceAppApiTestCase; + /** * Class OAuth2UserServiceApiTest */ -final class OAuth2UserServiceApiTest extends OAuth2ProtectedApiTestCase { +final class OAuth2UserServiceApiTest extends OAuth2ProtectedServiceAppApiTestCase { public function testUpdateMe(){ @@ -102,6 +107,44 @@ public function testGetAllWithoutFilter(){ $this->assertTrue($page->total > 0); } + public function testUpdateUserGroups(){ + $repo = EntityManager::getRepository(Group::class); + $group = $repo->getOneBySlug('raw-users'); + + $repo = EntityManager::getRepository(User::class); + $user = $repo->getAll()[0]; + + $params = [ + 'id' => $user->getId() + ]; + + $data = [ + 'groups' => [$group->getId()], + ]; + + $headers = [ + "HTTP_Authorization" => " Bearer " . $this->access_token_service_app_type, + "CONTENT_TYPE" => "application/json" + ]; + + $this->action( + "PUT", + "Api\OAuth2\OAuth2UserApiController@updateUserGroups", + $params, + [], + [], + [], + $headers, + json_encode($data) + ); + + $this->assertResponseStatus(201); + + $user = $repo->getById($user->getId()); + $this->assertNotNull($user); + $this->assertCount(1, $user->getGroups()); + } + protected function getScopes() { $scope = array( @@ -109,7 +152,8 @@ protected function getScopes() IUserService::UserProfileScope_Email, IUserService::UserProfileScope_Profile, IUserScopes::MeWrite, - IUserScopes::ReadAll + IUserScopes::ReadAll, + IUserScopes::UserGroupWrite ); return $scope;