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
32 changes: 0 additions & 32 deletions .github/workflows/check-coding-standards.yml

This file was deleted.

74 changes: 74 additions & 0 deletions .github/workflows/laravel.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
name: CI

on:
push:
branches: [ 3.x, 4.x ]
pull_request:
branches: [ 3.x, 4.x ]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
laravel:
runs-on: ubuntu-latest

services:
postgres:
image: postgres:17
env:
POSTGRES_USER: seatplus
POSTGRES_PASSWORD: secret
POSTGRES_DB: laravel
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5

redis:
image: redis:7
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5

steps:
- uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: mbstring, dom, fileinfo, pgsql, pdo_pgsql, redis
coverage: xdebug

- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: vendor
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-

- name: Install Dependencies
run: composer install --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist

- name: Check Coding Standards
run: composer run test:lint

- name: Static Analysis
run: composer run test:types

- name: Type Coverage
run: composer run test:type-coverage

- name: Run Tests
env:
XDEBUG_MODE: coverage
run: vendor/bin/pest --coverage --min=100 --colors=always
46 changes: 0 additions & 46 deletions .github/workflows/tests.yml

This file was deleted.

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ build/
.php_cs
.php_cs.cache
.phpunit.result.cache
.phpunit.cache
.php-cs-fixer.cache

90 changes: 83 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,87 @@
# auth
handels authentication for web and eveapi
# seatplus/auth

# Usage
[![CI](https://github.com/seatplus/auth/actions/workflows/laravel.yml/badge.svg)](https://github.com/seatplus/auth/actions/workflows/laravel.yml)

## Add more scopes
By default the minimal scopes are requested for users. However one might add scopes to an existing user by adding
a query parameters stating comma separated which scopes should be add:
Handles authentication, authorisation, and SSO scope compliance for the seatplus EVE Online management platform. This is the core package — `seatplus/eveapi` and `seatplus/web` both depend on it.

## Overview

### Role system

Four role types with distinct membership and permission semantics:

| Type | Membership | Use case |
|------|-----------|---------|
| `automatic` | Auto-assigned when a character belongs to a configured corporation or alliance | Fleet / alliance access |
| `on-request` | User applies, moderator approves or denies | Corp-specific elevated access |
| `manual` | Admin explicitly adds / removes individual users | One-off grants |
| `opt-in` | User self-joins if they meet the criteria | Opt-in programmes |

### Affiliation system

Every role has `Affiliation` records that define **permission scope** (which EVE entities the role holder can access data for), not membership. Three types:

- `allowed` — these corporations / alliances / characters are in scope
- `inverse` — everyone *except* these is in scope
- `forbidden` — always excluded, overrides `allowed` / `inverse`

### SSO scope compliance

`IsUserCompliantService` checks whether every character owned by a user has all required OAuth scopes. Required scopes are aggregated from global settings, corporation-level `SsoScopes` records, and alliance-level records. Non-compliant users have their role memberships set to `inactive` automatically on the next `handleMembers()` call.

### Permission checking

`CanUserService::check()` runs a Laravel Pipeline to validate a set of EVE entity IDs against a user's permissions. The pipeline strips IDs the user owns, IDs covered by in-game corporation roles (e.g. Director), and IDs covered by Spatie permissions. Any remaining IDs are denied. The `superuser` permission bypasses all checks.

## Installation

```bash
composer require seatplus/auth
```

Publish and run migrations:

```bash
php artisan vendor:publish --provider="Seatplus\Auth\AuthServiceProvider"
php artisan migrate
```
/eve/sso/{character_id?}/step_up?add_scopes=scope1,scope2

## Usage

### Add OAuth scopes to a character

By default the minimal scopes are requested. To step up a character to additional scopes, redirect to:

```
/eve/sso/{character_id}/step_up?add_scopes=esi-skills.read_skills.v1,esi-wallet.read_character_wallet.v1
```

### Check permissions

```php
use Seatplus\Auth\Services\Dtos\ValidateIdsDTO;
use Seatplus\Auth\Services\CanUserService;

$dto = ValidateIdsDTO::make(entity_ids: [12345678], user: $user);
CanUserService::check($user, $dto, permissions: ['view member tracking']);
```

## Development

### Requirements

- PHP 8.3+
- PostgreSQL (user `seatplus`, password `secret`, database `laravel` @ `127.0.0.1:5432`)
- Redis @ `127.0.0.1:6379`

### Running the test suite

```bash
composer run test # lint + PHPStan + type-coverage + unit tests
composer run test:unit # unit tests only
composer run test:lint # Pint formatting check
composer run lint # auto-fix formatting with Pint
composer run test:types # PHPStan static analysis
composer run test:type-coverage # 100% type coverage check
```

3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
"nunomaduro/collision": "^8.1",
"pestphp/pest": "^3.0",
"pestphp/pest-plugin-laravel": "^3.0",
"pestphp/pest-plugin-type-coverage": "^3.1",
"pestphp/pest-plugin-type-coverage": "3.5.1",
"phpstan/phpstan": "1.12.24",
"rector/rector": "^1.2",
"driftingly/rector-laravel": "^1.2",
"larastan/larastan": "^2.9",
Expand Down
9 changes: 6 additions & 3 deletions config/permission.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<?php

use Seatplus\Auth\Models\Permissions\Permission;
use Seatplus\Auth\Models\Permissions\Role;

return [

'models' => [
Expand All @@ -13,7 +16,7 @@
* `Spatie\Permission\Contracts\Permission` contract.
*/

'permission' => Seatplus\Auth\Models\Permissions\Permission::class,
'permission' => Permission::class,

/*
* When using the "HasRoles" trait from this package, we need to know which
Expand All @@ -24,7 +27,7 @@
* `Spatie\Permission\Contracts\Role` contract.
*/

'role' => Seatplus\Auth\Models\Permissions\Role::class,
'role' => Role::class,

],

Expand Down Expand Up @@ -167,7 +170,7 @@
* When permissions or roles are updated the cache is flushed automatically.
*/

'expiration_time' => \DateInterval::createFromDateString('24 hours'),
'expiration_time' => DateInterval::createFromDateString('24 hours'),

/*
* The cache key used to store all permissions.
Expand Down
6 changes: 4 additions & 2 deletions src/AuthenticationServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
use Laravel\Socialite\SocialiteManager;
use Seatplus\Auth\Listeners\ReactOnFreshRefreshToken;
use Seatplus\Auth\Listeners\UpdatingRefreshTokenListener;
use Seatplus\Auth\Models\Permissions\Permission;
use Seatplus\Auth\Models\Permissions\Role;
use Seatplus\Auth\Models\User;
use Seatplus\Auth\Observers\ApplicationObserver;
use Seatplus\Auth\Observers\CharacterAffiliationObserver;
Expand Down Expand Up @@ -102,8 +104,8 @@ function (Container $app) use ($socialite) {
$this->mergeConfigFrom(__DIR__.'/../config/auth.services.php', 'services');

config()->set('permission.models', [
'permission' => \Seatplus\Auth\Models\Permissions\Permission::class,
'role' => \Seatplus\Auth\Models\Permissions\Role::class,
'permission' => Permission::class,
'role' => Role::class,
]);

$this->setUserModel();
Expand Down
25 changes: 17 additions & 8 deletions src/Http/Actions/Roles/ManageAutomaticRoleAction.php
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
<?php

declare(strict_types=1);

namespace Seatplus\Auth\Http\Actions\Roles;

use Illuminate\Support\Arr;
use Seatplus\Auth\Enums\RoleType;
use Seatplus\Auth\Http\Requests\RoleRequest;
use Seatplus\Auth\Services\Roles\BaseRoleService;
use Seatplus\Auth\Services\Roles\DTO\AffiliationData;
use Seatplus\Auth\Services\Roles\DTO\CriteriaData;

class ManageAutomaticRoleAction
{
Expand All @@ -21,28 +25,33 @@ public function execute(RoleRequest $request): void
$this->checkPermission();

$validated = $request->validated();
$this->baseRoleService->for($validated['role_id']);
$roleService = $this->baseRoleService->for($validated['role_id'])->automatic();

$roleService = $this->baseRoleService->automatic();
// setRoleType first: if the type changes it calls resetRoleMemberships(),
// which would wipe any criteria written below.
$roleService->setRoleType(RoleType::AUTOMATIC);

if ($name = Arr::get($validated, 'name')) {
$roleService->updateRoleName($name);
}

if ($affiliated = Arr::get($validated, 'affiliated')) {
$roleService->syncAffiliateManyEntities($affiliated);
if (is_array($affiliated = Arr::get($validated, 'affiliated'))) {
$roleService->syncAffiliateManyEntities(
...array_map(fn (array $affiliationData) => AffiliationData::fromArray($affiliationData), $affiliated)
);
}

if ($assigned = Arr::get($validated, 'assigned')) {
$roleService->automaticallyAssignRoleTo($assigned);
if (is_array($assigned = Arr::get($validated, 'assigned'))) {
$roleService->automaticallyAssignRoleTo(
...array_map(fn (array $criteriaData) => CriteriaData::fromArray($criteriaData), $assigned)
);
}

$roleService->setRoleType(RoleType::AUTOMATIC);
$roleService->handleMembers();
}

private function checkPermission(): void
{

$auth = auth()->user();

throw_unless($auth, \Exception::class, 'User not authenticated');
Expand Down
Loading
Loading