Commit c80e754a authored by Jacob Priddy's avatar Jacob Priddy 👌
Browse files

Merge branch '74-d-o-c-u-m-e-n-t-a-t-i-o-n' into 'master'

Resolve "D O C U M E N T A T I O N"

Closes #74

See merge request !69
parents 573b782b 73fcb01d
Pipeline #11828 passed with stages
in 3 minutes and 2 seconds
# Thers a stupid bug in nginx that's been around for years that makes it so we can't easily put both front and backend
# on the same server. So we'll just do a proxy pass...
# Checkout https://ssl-config.mozilla.org/ for ocnfiguring secure ssl
# Checkout https://ssl-config.mozilla.org/ for configuring secure ssl
# Bug is you cannot use try_files with alias, and has been open for almost 10 years at this point...
upstream localhost.api {
server 127.0.0.1:443;
server 127.0.0.1:80;
}
server {
......@@ -71,7 +72,7 @@ server {
}
location /api {
proxy_pass https://localhost.api;
proxy_pass http://localhost.api;
}
location / {
......@@ -82,27 +83,9 @@ server {
server {
server_name localhost.api;
listen 443 ssl;
listen 80;
index index.php index.html;
ssl_certificate /run/secrets/webserver_cert;
ssl_certificate_key /run/secrets/webserver_key;
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
ssl_session_tickets off;
# curl https://ssl-config.mozilla.org/ffdhe2048.txt > /path/to/dhparam
ssl_dhparam /run/dhparam/dhparam;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# HSTS (ngx_http_headers_module is required) (63072000 seconds)
add_header Strict-Transport-Security "max-age=63072000" always;
ssl_trusted_certificate /run/secrets/root_cert;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
root /var/www/backend/public;
......
......@@ -3,9 +3,10 @@
# Thers a stupid bug in nginx that's been around for years that makes it so we can't easily put both front and backend
# on the same server. So we'll just do a proxy pass...
# Checkout https://ssl-config.mozilla.org/ for ocnfiguring secure ssl
# Checkout https://ssl-config.mozilla.org/ for configuring secure ssl
# Bug is you cannot use try_files with alias, and has been open for almost 10 years at this point...
upstream localhost.api {
server 127.0.0.1:443;
server 127.0.0.1:80;
}
server {
......@@ -61,7 +62,7 @@ server {
}
location /api {
proxy_pass https://localhost.api;
proxy_pass http://localhost.api;
}
location / {
......@@ -69,30 +70,11 @@ server {
}
}
server {
server_name localhost.api;
listen 443 ssl;
listen 80;
index index.php index.html;
ssl_certificate /run/secrets/webserver_cert;
ssl_certificate_key /run/secrets/webserver_key;
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
ssl_session_tickets off;
# curl https://ssl-config.mozilla.org/ffdhe2048.txt > /path/to/dhparam
ssl_dhparam /run/dhparam/dhparam;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# HSTS (ngx_http_headers_module is required) (63072000 seconds)
add_header Strict-Transport-Security "max-age=63072000" always;
ssl_trusted_certificate /run/secrets/root_cert;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
root /var/www/backend/public;
......
......@@ -7,7 +7,7 @@ APP_NAME=doorcode
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
APP_URL=https://elock.cs.wallawalla.edu
DB_CONNECTION=doorcode
DB_DRIVER=pgsql
......
......@@ -13,3 +13,5 @@ npm-debug.log
yarn-error.log
.php_cs.cache
cov/
public/docs
resources/docs
......@@ -20,11 +20,11 @@ class DummyClassRepositoryServiceProvider extends ServiceProvider implements Def
public function register()
{
$this->app->singleton(DummyClassRepository::class, static function (Application $app) {
if (env('APP_ENV') === 'memory') {
if (config('app.env') === 'memory') {
return new LocalDummyClassRepository();
}
if(env('APP_ENV') === 'testing') {
if(config('app.env') === 'testing') {
return new InMemoryDummyClassRepository();
}
......
<?php
namespace App\Documentation\Strategies;
use ReflectionClass;
use ReflectionMethod;
use Illuminate\Routing\Route;
use Source\Gateways\Doors\DoorsRepository;
use Source\Gateways\Groups\GroupsRepository;
use Source\Gateways\Tokens\TokensRepository;
use Mpociot\ApiDoc\Tools\DocumentationConfig;
use Source\Gateways\Doors\LocalDoorsRepository;
use Source\Gateways\Users\LocalUsersRepository;
use Illuminate\Contracts\Foundation\Application;
use Source\Gateways\DoorUser\DoorUserRepository;
use Source\Gateways\Groups\LocalGroupsRepository;
use Source\Gateways\Tokens\LocalTokensRepository;
use Mpociot\ApiDoc\Extracting\Strategies\Strategy;
use Source\Gateways\DoorGroup\DoorGroupRepository;
use Source\Gateways\GroupUser\GroupUserRepository;
use Source\Gateways\Schedules\SchedulesRepository;
use Source\Gateways\DoorUser\LocalDoorUserRepository;
use Source\Gateways\DoorGroup\LocalDoorGroupRepository;
use Source\Gateways\GroupUser\LocalGroupUserRepository;
use Source\Gateways\Schedules\LocalSchedulesRepository;
use Source\Gateways\DoorSchedule\DoorScheduleRepository;
use Source\Gateways\DoorSchedule\LocalDoorScheduleRepository;
class ApplicationRepositoryResetStrategy extends Strategy
{
/**
* @var \Illuminate\Contracts\Foundation\Application
*/
protected Application $app;
public function __construct(string $stage, DocumentationConfig $config)
{
$this->app = app();
parent::__construct($stage, $config);
}
/**
* @inheritDoc
*/
public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method, array $routeRules, array $context = [])
{
$users = new LocalUsersRepository();
$doors = new LocalDoorsRepository();
$groups = new LocalGroupsRepository();
$schedules = new LocalSchedulesRepository();
$groupUser = new LocalGroupUserRepository($users, $groups);
$doorGroups = new LocalDoorGroupRepository($doors, $groups);
$this->app->instance(DoorGroupRepository::class, $doorGroups);
$this->app->instance(DoorsRepository::class, $doors);
$this->app->instance(DoorScheduleRepository::class, new LocalDoorScheduleRepository($doors, $doorGroups, $schedules));
$this->app->instance(DoorUserRepository::class, new LocalDoorUserRepository($users, $doors, $groupUser, $doorGroups));
$this->app->instance(GroupsRepository::class, $groups);
$this->app->instance(GroupUserRepository::class, $groupUser);
$this->app->instance(SchedulesRepository::class, $schedules);
$this->app->instance(TokensRepository::class, new LocalTokensRepository());
return null;
}
}
<?php
namespace App\Documentation\Strategies;
use ReflectionClass;
use ReflectionMethod;
use Illuminate\Routing\Route;
use Mpociot\ApiDoc\Extracting\Strategies\Strategy;
class BodyAuthenticationStrategy extends Strategy
{
/**
* @inheritDoc
*/
public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method, array $routeRules, array $context = [])
{
if (in_array('GET', $route->methods(), true)) {
return null;
}
if (!$context['metadata']['authenticated']) {
return null;
}
$token = 'token_string_admin';
if ($controller->getShortName() === 'MeController') {
$token = 'token_string_engr';
}
return [
'api_token' => [
'type' => 'string',
'description' => 'The api authentication token to use. Can be used in place of a bearer token.',
'required' => false,
'value' => $token,
],
];
}
}
<?php
namespace App\Documentation\Strategies;
use ReflectionClass;
use ReflectionMethod;
use Illuminate\Routing\Route;
use Mpociot\ApiDoc\Extracting\Strategies\Strategy;
class GetAuthenticationStrategy extends Strategy
{
/**
* @inheritDoc
*/
public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method, array $routeRules, array $context = [])
{
if (!in_array('GET', $route->methods(), true)) {
return null;
}
if (!$context['metadata']['authenticated']) {
return null;
}
$token = 'token_string_admin';
if ($controller->getShortName() === 'MeController') {
$token = 'token_string_engr';
}
return [
'api_token' => [
'type' => 'string',
'description' => 'The api authentication token to use. Can be used in place of a bearer token.',
'required' => false,
'value' => $token,
],
];
}
}
<?php
namespace App\Documentation\Strategies;
use ReflectionClass;
use ReflectionMethod;
use Illuminate\Routing\Route;
use Mpociot\ApiDoc\Extracting\Strategies\Strategy;
class PaginationStrategy extends Strategy
{
/**
* @inheritDoc
*/
public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method, array $routeRules, array $context = [])
{
if (!in_array('GET', $route->methods(), true)) {
return null;
}
if (strpos($method->getDocComment(), "@paginated\n") === false) {
return null;
}
return [
'page' => [
'type' => 'integer',
'description' => 'The page of paginated data to get.',
'required' => false,
'value' => 1,
],
];
}
}
<?php
namespace App\Documentation\Strategies;
use ReflectionClass;
use ReflectionMethod;
use Illuminate\Routing\Route;
use Mpociot\ApiDoc\Extracting\Strategies\Strategy;
class UnauthenticatedResponseStrategy extends Strategy
{
/**
* @inheritDoc
*/
public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method, array $routeRules, array $context = [])
{
if (!$context['metadata']['authenticated']) {
return null;
}
return [
[
'content' => [
'message' => 'Unauthenticated',
],
'status' => 401,
],
[
'content' => [
'message' => 'Unauthorized',
],
'status' => 403,
]
];
}
}
......@@ -7,9 +7,29 @@ use Source\Authorization\Permissions;
use Source\UseCases\Attempts\GetAttempts\GetAttemptsUseCase;
use Source\UseCases\Attempts\APIPresenter as GetAttemptsAPIPresenter;
/**
* @group Door Attempt Logs
*
* APIs for viewing attempts on doors. An attempt is a failed code entry that did not match any users.
*/
class AttemptsController extends ApiController
{
/**
* Get Attempts
*
* This route filters attempts based off of starting date, ending date, or door id.
* If only start is supplied, all attempts after the start date are given. If only end
* is supplied, all attempts before the start date are given. If both dates are supplied,
* all attempts between the given dates are returned. This route is paginated.
*
* @authenticated
* @paginated
* @queryParam start The beginning date to filter attempts by. Example: 2000-06-02 08:11:45
* @queryParam end The ending date to filter attempts by. Example: 2920-06-02 08:11:45
* @queryParam door_id The door id to filter on. Example: 1
*
* @response 422 {"message":"The given data was invalid.","errors":{"start":["The start is not a valid date."],"end":["The end is not a valid date."],"door_id":["The door id must be an integer."]}}
*
* @param \Source\UseCases\Attempts\GetAttempts\GetAttemptsUseCase $attempts
* @return \Illuminate\Http\JsonResponse
* @throws \Illuminate\Validation\ValidationException
......@@ -22,9 +42,9 @@ class AttemptsController extends ApiController
$this->authorizer->protect(Permissions::LOGS_READ);
$this->validate($this->request, [
'start' => 'date',
'end' => 'date',
'door_id' => 'integer',
'start' => 'nullable|date',
'end' => 'nullable|date',
'door_id' => 'nullable|integer',
]);
$presenter = new GetAttemptsAPIPresenter();
......
......@@ -13,6 +13,11 @@ use Source\UseCases\Users\Authenticate\APIPresenter;
use Source\UseCases\Users\Authenticate\AuthenticateUseCase;
use Source\UseCases\Users\Authenticate\UserCreationException;
/**
* @group Authentication
*
* This set of routes deals with authentication with the application through SAML and application login.
*/
class AuthController extends ApiController
{
protected CookieJar $cookieJar;
......@@ -30,13 +35,29 @@ class AuthController extends ApiController
}
/**
* Login request to application
*
* This endpoint returns a token that can be used in other endpoints as well as setting a cookie.
* One does not need to make a request to this if they have a valid token.
*
* @bodyParam email string required The email of the login user. Example: sithL0rd@senate.com
* @bodyParam password string required The password of the user to login as. Example: I am the senate
*
* @response 422 {"message":"The given data was invalid.","errors":{"email":["The email field is required."],"password":["The password field is required."]}}
*
* @param AuthenticateUseCase $authenticateUseCase
* @return JsonResponse
* @throws AuthenticationException
* @throws EntityNotFoundException
* @throws \Illuminate\Validation\ValidationException
*/
public function login(AuthenticateUseCase $authenticateUseCase): JsonResponse
{
$this->validate($this->request, [
'email' => 'required|string|email',
'password' => 'required|string',
]);
$presenter = new APIPresenter();
$authenticateUseCase->attempt($presenter, $this->request->all());
......@@ -51,6 +72,10 @@ class AuthController extends ApiController
}
/**
* Start a saml login request
*
* This route redirects the user to the running SAML authentication instance to start authentication with SAML
*
* @param \Source\UseCases\Users\Authenticate\AuthenticateUseCase $authenticateUseCase
* @return \Illuminate\Http\RedirectResponse
*/
......@@ -60,9 +85,14 @@ class AuthController extends ApiController
}
/**
* Handle SAML login
*
* This API is only meant to be used by SAML after a return from a login.
*
* @param AuthenticateUseCase $authenticateUseCase
* @return mixed
* @throws EntityNotFoundException
* @throws \Source\Exceptions\EntityExistsException
*/
public function handle(AuthenticateUseCase $authenticateUseCase)
{
......@@ -71,8 +101,9 @@ class AuthController extends ApiController
try {
$authenticateUseCase->handleSamlLogin($presenter);
} catch (UserCreationException $e) {
$this->setStatusCode(400);
return $this->respondWithError(
'There was an error authenticating the user. Please contact an administrator.'
'Invalid SAML user given. If you believe this is in error, please contact an administrator.'
);
}
......@@ -86,6 +117,10 @@ class AuthController extends ApiController
}
/**
* Log out
*
* This endpoint logs out of saml and expires the associated api/login token and cookie.
*
* @param AuthenticateUseCase $authenticateUseCase
* @return RedirectResponse
*/
......
......@@ -14,6 +14,13 @@ use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Source\UseCases\Door\UpdateBinary\UpdateBinaryUseCase;
use Source\UseCases\Door\StatusResponse\StatusResponseUseCase;
/**
* @group Door Routes
*
* Set of routes for door clients wanting to protect access to doors.
* Includes routes for verifying a doorcode as well as getting open mode times from schedules and overrides
* A header of Door-Controller-Version can be set with the the door client version.
*/
class DoorController extends ApiController
{
/**
......@@ -35,15 +42,22 @@ class DoorController extends ApiController
/**
* @return \Illuminate\Http\JsonResponse
* @throws \Illuminate\Validation\ValidationException
*/
protected function respondStatus(): JsonResponse
{
$this->validate($this->request, [
'foresight' => 'integer',
]);
$foresight = $this->request->input('foresight') ?? config('app.status_foresight');
$presenter = new JsonPresenter();
$this->response->getStatusForDoor(
$this->doorGuard->id(),
Carbon::now(),
Carbon::now()->addMinutes(config('app.status_foresight')),
Carbon::now()->addMinutes((int)$foresight),
$presenter
);
......@@ -51,11 +65,27 @@ class DoorController extends ApiController
}
/**
* Door Access
*
* Checks to see if the given doorcode can currently access the authenticated door.
* Also processes commands that are separated by a '*' from the doorcode. If a command is accepted a 200 is
* returned. If a command is rejected, a 403 is given.
*
* @authenticated
* @urlParam doorcode required The doorcode to query. Example: 123456*00110
* @queryParam foresight Number of minutes ahead of now to get the open mode times for. Example: 720
*
* @response 422
* {"message":"The given data was invalid.","errors":{"foresight":["The foresight must be an integer."]}}
* @response 403
* {"events":[{"begins_at":"2020-06-03T11:54:07-07:00","ends_at":"2020-06-03T11:55:07-07:00"},{"begins_at":"2020-06-03T11:55:07-07:00","ends_at":"2020-06-03T12:15:07-07:00"},{"begins_at":"2020-06-03T21:54:07-07:00","ends_at":"2020-06-03T21:55:07-07:00"}]}
*
* @param string $doorcode
* @param \Source\UseCases\Door\Access\AccessUseCase $access
* @return \Illuminate\Http\JsonResponse
* @throws \Source\Exceptions\AuthenticationException
* @throws \Source\Exceptions\AuthorizationException
* @throws \Illuminate\Validation\ValidationException
*/
public function access(string $doorcode, AccessUseCase $access): JsonResponse
{
......@@ -65,7 +95,20 @@ class DoorController extends ApiController
}
/**
* Door Open Times
*
* This route returns the times when the authenticated door client is supposed to go into open mode accepting any
* key press as a valid door unlock. Retrieves the open mode times for the next interval. Includes open mode
* schedules as well as overrides. The door to get the times for is based off of the authenticated door.
*
* @authenticated
* @queryParam foresight Number of minutes ahead of now to get the open mode times for. Example: 720
*
* @response 422
* {"message":"The given data was invalid.","errors":{"foresight":["The foresight must be an integer."]}}
*
* @return \Illuminate\Http\JsonResponse
* @throws \Illuminate\Validation\ValidationException
*/
public function status(): JsonResponse
{
......@@ -73,6 +116,15 @@ class DoorController extends ApiController
}
/**
* Door Update
*
* This route returns the newest binary that the door controllers should be running based upon the authenticated
* door. If there are no binaries on record, a 404 response is returned.
*
* @authenticated
*
* @response 404 {"status":"error","code":404,"message":"Entity not found"}
*
* @param \Source\UseCases\Door\UpdateBinary\UpdateBinaryUseCase $updateBinaryUseCase
* @param \App\Guards\DoorGuard $doorGuard
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse
......
......@@ -22,9 +22,24 @@ use Source\UseCases\DoorGroup\GetDoorGroups\APIPresenter as GetDoorGroupsAPIPres
use Source\UseCases\DoorGroup\AddDoorToGroup\APIPresenter as AddDoorToGroupAPIPresenter;
use Source\UseCases\DoorGroup\RemoveDoorFromGroup\APIPresenter as RemoveDoorFromGroupAPIPresenter;
/**
* @group Door Management
*
* This set of endpoints deals with the management of doors in the system. These route require the
* manage doors permission.
*/
class DoorsController extends ApiController