diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8786698..2c24de2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,10 @@ name: CI on: ['push', 'pull_request'] +permissions: + contents: read + packages: write + jobs: tests: runs-on: ubuntu-latest @@ -47,12 +51,13 @@ jobs: run: vendor/bin/phpunit docker: + # Only push Docker image for direct pushes to master branch + if: github.event_name == 'push' && github.ref == 'refs/heads/master' runs-on: ubuntu-latest needs: tests - if: github.event_name == 'push' || github.event_name == 'pull_request' steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Log in to the Container registry uses: docker/login-action@v3 with: diff --git a/Dockerfile b/Dockerfile index f8480f4..2d507df 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,32 +1,39 @@ FROM php:8.1-alpine -RUN apk add --no-cache git jq moreutils -RUN apk add --no-cache $PHPIZE_DEPS postgresql-dev \ - && docker-php-ext-install pdo_pgsql \ +# Base tools and PHP extensions +RUN apk add --no-cache git jq moreutils \ + && apk add --no-cache $PHPIZE_DEPS postgresql-dev sqlite-dev \ + && docker-php-ext-install pdo_pgsql pdo_sqlite \ && pecl install xdebug-3.1.5 \ && docker-php-ext-enable xdebug \ && echo "xdebug.mode=debug" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \ && echo "xdebug.client_host = 172.19.0.1" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini +# Composer + fresh Laravel app RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer -RUN composer create-project --prefer-dist laravel/laravel example && \ - cd example +RUN composer create-project --prefer-dist laravel/laravel example && cd example WORKDIR /example +# Link local package COPY . /laravel-scim-server RUN jq '.repositories=[{"type": "path","url": "/laravel-scim-server"}]' ./composer.json | sponge ./composer.json +# Install package and dev helpers RUN composer require arietimmerman/laravel-scim-server @dev && \ composer require laravel/tinker +# SQLite config RUN touch /example/database.sqlite && \ echo "DB_CONNECTION=sqlite" >> /example/.env && \ echo "DB_DATABASE=/example/database.sqlite" >> /example/.env && \ echo "APP_URL=http://localhost:18123" >> /example/.env +# Make users.password nullable to allow SCIM-created users without passwords +RUN sed -i -E "s/\\$table->string\('password'\);/\\$table->string('password')->nullable();/g" \ + database/migrations/*create_users_table.php || true -# Add migration for groups table using heredoc +# Groups table migration RUN cat > /example/database/migrations/2021_01_01_000001_create_groups_table.php <<'EOM' app/Models/Group.php <<'EOM' belongsToMany(User::class, 'group_user', 'group_id', 'user_id')->withTimestamps(); + } } EOM -# Add Custom SCIM Config overriding Group model class +# Override SCIM config to use app's Group model RUN mkdir -p app/SCIM && cat > app/SCIM/CustomSCIMConfig.php <<'EOM' app/Providers/AppServiceProvider.php <<'EOM' database/factories/GroupFactory.php <<'EOM' /example/database/migrations/2021_01_01_000002_create_group_user_table.php <<'EOM' +id(); + $table->foreignId('group_id')->constrained('groups')->cascadeOnDelete(); + $table->foreignId('user_id')->constrained('users')->cascadeOnDelete(); + $table->timestamps(); + $table->unique(['group_id', 'user_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('group_user'); + } +}; +EOM + +# Ensure User model has groups() relation (overwrite default) +RUN cat > app/Models/User.php <<'EOM' + 'datetime', + ]; + + public function groups(): BelongsToMany + { + return $this->belongsToMany(Group::class, 'group_user', 'user_id', 'group_id')->withTimestamps(); + } +} +EOM + +# Seeder for demo data +RUN cat > /example/database/seeders/DemoSeeder.php <<'EOM' +count(50)->create(); + $groups = Group::factory()->count(10)->create(); + + foreach ($groups as $g) { + $g->members()->sync($users->random(rand(3, 10))->pluck('id')->toArray()); + } + } +} +EOM + # Run migrations and seed demo data -RUN php artisan migrate && \ - echo "User::factory()->count(100)->create(); App\\Models\\Group::factory()->count(10)->create();" | php artisan tinker +RUN php artisan migrate && php artisan db:seed --class=Database\\Seeders\\DemoSeeder CMD ["php","artisan","serve","--host=0.0.0.0","--port=8000"] + diff --git a/laravel-scim-server.svg b/laravel-scim-server.svg index 638af5b..1d79d20 100644 --- a/laravel-scim-server.svg +++ b/laravel-scim-server.svg @@ -2,50 +2,102 @@ - + id="defs2"> + + - - + transform="translate(-1.748224,-1.0010848)"> + + + + + + + + + + + + + + + + + + + + diff --git a/src/SCIMConfig.php b/src/SCIMConfig.php index 1fd6cb4..e8931db 100644 --- a/src/SCIMConfig.php +++ b/src/SCIMConfig.php @@ -76,6 +76,10 @@ protected function doRead(&$object, $attributes = []) { return (string)$object->id; } + public function remove($value, &$object, $path = null) + { + // do nothing + } } ), new Meta('Users'), @@ -143,7 +147,7 @@ public function getGroupConfig() { return [ - 'class' => Group::class, + 'class' => $this->getGroupClass(), 'singular' => 'Group', //eager loading @@ -165,13 +169,17 @@ protected function doRead(&$object, $attributes = []) { return (string)$object->id; } + public function remove($value, &$object, $path = null) + { + // do nothing + } } ), new Meta('Groups'), (new AttributeSchema(Schema::SCHEMA_GROUP, true))->withSubAttributes( eloquent('displayName')->ensure('required', 'min:3', function ($attribute, $value, $fail) { // check if group does not exist or if it exists, it is the same group - $group = Group::where('displayName', $value)->first(); + $group = $this->getGroupClass()::where('displayName', $value)->first(); if ($group && (request()->route('resourceObject') == null || $group->id != request()->route('resourceObject')->id)) { $fail('The name has already been taken.'); }