From 9abe4b219610595520cc69125c2c4a483e611ba9 Mon Sep 17 00:00:00 2001 From: smarcet Date: Thu, 6 Feb 2014 19:51:58 -0300 Subject: [PATCH] Implements: blueprint openid-oauth2-user-service [smarcet] - #5029 - UserService Change-Id: Ie4da1f28810e7562a9dc9ceb06228040848eebdf --- app/config/app.php | 8 +- app/config/cors.php | 23 + app/config/local/app.php | 4 + app/config/server.php | 51 ++ app/controllers/AdminController.php | 2 + app/controllers/UserController.php | 2 +- .../apis/ApiBannedIPController.php | 7 + .../apis/ApiEndpointController.php | 7 +- app/controllers/apis/ApiScopeController.php | 8 +- app/controllers/apis/ClientApiController.php | 113 ++++- app/controllers/apis/JsonController.php | 2 +- app/controllers/apis/UserApiController.php | 12 +- .../protected/OAuth2ProtectedController.php | 2 +- .../protected/OAuth2UserApiController.php | 33 ++ ...create_oauth2_clients_authorized_realm.php | 38 -- ...46_create_oauth2_client_allowed_origin.php | 45 ++ ...014_02_07_142007_update_oauth2_client.php} | 5 +- ...2_07_142031_update_oauth2_api_endpoint.php | 32 ++ app/database/seeds/ApiEndpointSeeder.php | 459 +----------------- app/database/seeds/ApiScopeSeeder.php | 301 +----------- app/database/seeds/ApiSeeder.php | 42 +- app/database/seeds/ResourceServerSeeder.php | 9 +- app/database/seeds/TestSeeder.php | 77 ++- app/filters.php | 22 +- .../OAuth2RequestAccessTokenValidator.php | 31 +- app/lang/en/validation.php | 2 + .../auth/AuthenticationServiceProvider.php | 12 +- app/libs/auth/CustomAuthProvider.php | 25 +- app/libs/auth/User.php | 40 +- app/libs/oauth2/IResourceServerContext.php | 33 +- app/libs/oauth2/OAuth2Protocol.php | 11 + app/libs/oauth2/OAuth2ServiceProvider.php | 11 +- .../exceptions/InvalidClientCredentials.php | 12 + .../oauth2/grant_types/AbstractGrantType.php | 7 +- .../ValidateBearerTokenGrantType.php | 4 +- app/libs/oauth2/models/AuthorizationCode.php | 2 +- app/libs/oauth2/models/IApiEndpoint.php | 5 + app/libs/oauth2/models/IClient.php | 107 +++- app/libs/oauth2/models/RefreshToken.php | 2 +- .../oauth2/resource_server/IUserService.php | 26 + .../OAuth2ProtectedService.php | 27 ++ .../OAuth2AccessTokenValidationResponse.php | 1 + .../oauth2/services/IAllowedOriginService.php | 17 + .../oauth2/services/IApiEndpointService.php | 13 +- app/libs/oauth2/services/IClientService.php | 27 +- .../oauth2/services/OAuth2ServiceCatalog.php | 1 + .../OAuth2ResponseStrategyFactoryMethod.php | 8 +- app/libs/openid/OpenIdProtocol.php | 23 +- app/libs/openid/OpenIdServiceProvider.php | 22 +- .../openid/extensions/OpenIdExtension.php | 14 +- .../implementations/OpenIdAXExtension.php | 16 +- .../implementations/OpenIdAXRequest.php | 2 - .../implementations/OpenIdOAuth2Extension.php | 16 +- .../implementations/OpenIdPAPEExtension.php | 15 +- .../implementations/OpenIdSREGExtension.php | 9 +- .../implementations/OpenIdSREGRequest.php | 12 +- .../SessionAssociationDHStrategy.php | 8 +- .../SessionAssociationUnencryptedStrategy.php | 8 +- app/libs/openid/model/IOpenIdUser.php | 4 + app/libs/openid/model/OpenIdNonce.php | 7 +- .../requests/OpenIdAuthenticationRequest.php | 4 +- .../OpenIdCheckAuthenticationRequest.php | 6 +- app/libs/openid/requests/OpenIdRequest.php | 4 +- app/libs/openid/services/IUserService.php | 1 + .../OpenIdResponseStrategyFactoryMethod.php | 6 +- app/libs/utils/IPHelper.php | 22 + app/libs/utils/services/ICacheService.php | 2 + app/libs/utils/services/Registry.php | 42 -- app/libs/utils/services/ServiceLocator.php | 32 ++ app/models/oauth2/ApiEndpoint.php | 10 +- app/models/oauth2/Client.php | 63 ++- app/models/oauth2/ClientAllowedOrigin.php | 12 + app/models/oauth2/ClientAuthorizedUri.php | 10 +- app/routes.php | 26 +- app/services/IPHelper.php | 14 - .../IUserActionService.php | 0 app/services/ServicesProvider.php | 125 ++--- .../ServerConfigurationService.php | 2 +- app/services/oauth2/AllowedOriginService.php | 76 +++ app/services/oauth2/ApiEndpointService.php | 18 +- app/services/oauth2/CORS/CORSMiddleware.php | 203 ++++++++ app/services/oauth2/CORS/CORSProvider.php | 28 ++ .../oauth2/CORS/cors_server_flowchart.png | Bin 0 -> 99960 bytes app/services/oauth2/ClientService.php | 133 +++-- app/services/oauth2/OAuth2ServiceProvider.php | 41 ++ app/services/oauth2/ResourceServerContext.php | 20 +- .../RevokeAuthorizationCodeRelatedTokens.php | 6 +- app/services/oauth2/TokenService.php | 2 +- .../oauth2/resource_server/UserService.php | 73 +++ app/services/openid/AssociationService.php | 2 +- .../{ => openid}/AuthenticationStrategy.php | 6 +- app/services/openid/MementoRequestService.php | 2 +- app/services/openid/NonceService.php | 17 +- app/services/openid/OpenIdProvider.php | 33 ++ .../openid/ServerExtensionsService.php | 13 +- app/services/openid/TrustedSitesService.php | 2 +- app/services/{ => openid}/UserService.php | 5 +- .../AbstractBlacklistSecurityPolicy.php | 1 + .../BlacklistSecurityPolicy.php | 2 +- .../security_policies/DelayCounterMeasure.php | 1 + .../LockUserCounterMeasure.php | 8 +- .../OAuth2LockClientCounterMeasure.php | 4 +- .../OAuth2SecurityPolicy.php | 11 +- .../{ => utils}/CheckPointService.php | 6 +- .../LockManagerService.php | 2 +- app/services/{ => utils}/LogService.php | 2 +- .../{ => utils}/RedisCacheService.php | 12 +- .../ServerConfigurationService.php | 96 ++-- app/services/utils/UtilsProvider.php | 50 ++ app/start/global.php | 8 +- app/strategies/DefaultLoginStrategy.php | 2 +- app/strategies/DirectResponseStrategy.php | 3 - app/strategies/OAuth2ConsentStrategy.php | 11 +- app/strategies/OpenIdConsentStrategy.php | 2 +- app/strategies/OpenIdLoginStrategy.php | 2 +- app/strategies/StrategyProvider.php | 17 +- app/tests/OAuth2UserServiceApiTest.php | 103 ++++ app/validators/CustomValidator.php | 36 ++ app/views/admin/server-config.blade.php | 3 +- app/views/extensions/oauth2.blade.php | 2 +- app/views/oauth2/consent.blade.php | 6 +- .../oauth2/profile/admin/edit-api.blade.php | 6 +- .../profile/admin/edit-endpoint.blade.php | 13 +- app/views/oauth2/profile/clients.blade.php | 61 ++- .../edit-client-allowed-origins.blade.php | 158 ++++++ .../edit-client-redirect-uris.blade.php | 56 ++- .../profile/edit-client-tokens.blade.php | 24 +- .../oauth2/profile/edit-client.blade.php | 16 +- bootstrap/start.php | 11 +- public/css/main.css | 15 +- ...uery.validate.additional.custom.methods.js | 8 +- 131 files changed, 2312 insertions(+), 1391 deletions(-) create mode 100644 app/config/cors.php create mode 100644 app/config/server.php create mode 100644 app/controllers/apis/protected/OAuth2UserApiController.php delete mode 100644 app/database/migrations/2013_12_04_182948_create_oauth2_clients_authorized_realm.php create mode 100644 app/database/migrations/2014_02_07_141946_create_oauth2_client_allowed_origin.php rename app/database/migrations/{2014_02_03_132103_update_oauth2_client.php => 2014_02_07_142007_update_oauth2_client.php} (87%) create mode 100644 app/database/migrations/2014_02_07_142031_update_oauth2_api_endpoint.php create mode 100644 app/libs/oauth2/exceptions/InvalidClientCredentials.php create mode 100644 app/libs/oauth2/resource_server/IUserService.php create mode 100644 app/libs/oauth2/resource_server/OAuth2ProtectedService.php create mode 100644 app/libs/oauth2/services/IAllowedOriginService.php create mode 100644 app/libs/utils/IPHelper.php delete mode 100644 app/libs/utils/services/Registry.php create mode 100644 app/libs/utils/services/ServiceLocator.php create mode 100644 app/models/oauth2/ClientAllowedOrigin.php delete mode 100644 app/services/IPHelper.php rename app/services/{security_policies => }/IUserActionService.php (100%) rename app/services/{Facades => facades}/ServerConfigurationService.php (87%) create mode 100644 app/services/oauth2/AllowedOriginService.php create mode 100644 app/services/oauth2/CORS/CORSMiddleware.php create mode 100644 app/services/oauth2/CORS/CORSProvider.php create mode 100644 app/services/oauth2/CORS/cors_server_flowchart.png create mode 100644 app/services/oauth2/OAuth2ServiceProvider.php create mode 100644 app/services/oauth2/resource_server/UserService.php rename app/services/{ => openid}/AuthenticationStrategy.php (86%) create mode 100644 app/services/openid/OpenIdProvider.php rename app/services/{ => openid}/UserService.php (98%) rename app/services/{ => utils}/CheckPointService.php (92%) rename app/services/{security_policies => utils}/LockManagerService.php (96%) rename app/services/{ => utils}/LogService.php (95%) rename app/services/{ => utils}/RedisCacheService.php (94%) rename app/services/{ => utils}/ServerConfigurationService.php (54%) create mode 100644 app/services/utils/UtilsProvider.php create mode 100644 app/tests/OAuth2UserServiceApiTest.php create mode 100644 app/views/oauth2/profile/edit-client-allowed-origins.blade.php diff --git a/app/config/app.php b/app/config/app.php index 0609a999..76e57fe0 100644 --- a/app/config/app.php +++ b/app/config/app.php @@ -78,7 +78,7 @@ return array( | */ - 'providers' => array( + 'providers' => array( 'Illuminate\Foundation\Providers\ArtisanServiceProvider', 'Illuminate\Auth\AuthServiceProvider', 'Illuminate\Cache\CacheServiceProvider', @@ -105,13 +105,17 @@ return array( 'Illuminate\View\ViewServiceProvider', 'Illuminate\Workbench\WorkbenchServiceProvider', 'Illuminate\Redis\RedisServiceProvider', + 'services\utils\UtilsProvider', + 'services\openid\OpenIdProvider', + 'services\oauth2\OAuth2ServiceProvider', 'auth\AuthenticationServiceProvider', 'services\ServicesProvider', 'strategies\StrategyProvider', 'oauth2\OAuth2ServiceProvider', 'openid\OpenIdServiceProvider', 'Greggilbert\Recaptcha\RecaptchaServiceProvider', - ), + 'services\oauth2\CORS\CORSProvider', + ), /* |-------------------------------------------------------------------------- diff --git a/app/config/cors.php b/app/config/cors.php new file mode 100644 index 00000000..f7e7a935 --- /dev/null +++ b/app/config/cors.php @@ -0,0 +1,23 @@ + 'true', + /** + * http://www.w3.org/TR/cors/#access-control-max-age-response-header + */ + 'UsePreflightCaching' => true, + 'MaxAge' => 32000, + /** + * http://www.w3.org/TR/cors/#access-control-allow-headers-response-header + */ + 'AllowedHeaders' => 'origin, content-type, accept, authorization', + /** + * http://www.w3.org/TR/cors/#access-control-allow-methods-response-header + */ + 'AllowedMethods' => 'GET, POST, OPTIONS, PUT, DELETE', +); \ No newline at end of file diff --git a/app/config/local/app.php b/app/config/local/app.php index e01af6fa..d235717a 100644 --- a/app/config/local/app.php +++ b/app/config/local/app.php @@ -105,12 +105,16 @@ return array( 'Illuminate\View\ViewServiceProvider', 'Illuminate\Workbench\WorkbenchServiceProvider', 'Illuminate\Redis\RedisServiceProvider', + 'services\utils\UtilsProvider', + 'services\openid\OpenIdProvider', + 'services\oauth2\OAuth2ServiceProvider', 'auth\AuthenticationServiceProvider', 'services\ServicesProvider', 'strategies\StrategyProvider', 'oauth2\OAuth2ServiceProvider', 'openid\OpenIdServiceProvider', 'Greggilbert\Recaptcha\RecaptchaServiceProvider', + 'services\oauth2\CORS\CORSProvider', ), /* diff --git a/app/config/server.php b/app/config/server.php new file mode 100644 index 00000000..abb900c2 --- /dev/null +++ b/app/config/server.php @@ -0,0 +1,51 @@ + 'http://www.openstack.org/', + 'MaxFailed_Login_Attempts' => 10, + 'MaxFailed_LoginAttempts_2ShowCaptcha' => 3, + //openid default values + 'OpenId_Private_Association_Lifetime' => 240, + 'OpenId_Session_Association_Lifetime' => 21600, + 'OpenId_Nonce_Lifetime' => 360, + /** + * Security Policies Configuration + */ + 'BlacklistSecurityPolicy_BannedIpLifeTimeSeconds' => 21600, + 'BlacklistSecurityPolicy_MinutesWithoutExceptions' => 5, + 'BlacklistSecurityPolicy_ReplayAttackExceptionInitialDelay' => 10, + 'BlacklistSecurityPolicy_MaxInvalidNonceAttempts' => 10, + 'BlacklistSecurityPolicy_InvalidNonceInitialDelay' => 10, + 'BlacklistSecurityPolicy_MaxInvalidOpenIdMessageExceptionAttempts' => 10, + 'BlacklistSecurityPolicy_InvalidOpenIdMessageExceptionInitialDelay' => 10, + 'BlacklistSecurityPolicy_MaxOpenIdInvalidRealmExceptionAttempts' => 10, + 'BlacklistSecurityPolicy_OpenIdInvalidRealmExceptionInitialDelay' => 10, + 'BlacklistSecurityPolicy_MaxInvalidOpenIdMessageModeAttempts' => 10, + 'BlacklistSecurityPolicy_InvalidOpenIdMessageModeInitialDelay' => 10, + 'BlacklistSecurityPolicy_MaxInvalidOpenIdAuthenticationRequestModeAttempts' => 10, + 'BlacklistSecurityPolicy_InvalidOpenIdAuthenticationRequestModeInitialDelay' => 10, + 'BlacklistSecurityPolicy_MaxAuthenticationExceptionAttempts' => 10, + 'BlacklistSecurityPolicy_AuthenticationExceptionInitialDelay' => 20, + 'BlacklistSecurityPolicy_MaxInvalidAssociationAttempts' => 10, + 'BlacklistSecurityPolicy_InvalidAssociationInitialDelay' => 20, + 'BlacklistSecurityPolicy_OAuth2_MaxAuthCodeReplayAttackAttempts' => 3, + 'BlacklistSecurityPolicy_OAuth2_AuthCodeReplayAttackInitialDelay' => 10, + 'BlacklistSecurityPolicy_OAuth2_MaxInvalidAuthorizationCodeAttempts' => 3, + 'BlacklistSecurityPolicy_OAuth2_InvalidAuthorizationCodeInitialDelay' => 10, + 'BlacklistSecurityPolicy_OAuth2_MaxInvalidBearerTokenDisclosureAttempt' => 3, + 'BlacklistSecurityPolicy_OAuth2_BearerTokenDisclosureAttemptInitialDelay' => 10, + //oauth2 default config values + 'OAuth2_AuthorizationCode_Lifetime' => 240, + 'OAuth2_AccessToken_Lifetime' => 3600, + 'OAuth2_RefreshToken_Lifetime' => 0, + //oauth2 security policy configuration + 'OAuth2SecurityPolicy_MinutesWithoutExceptions' => 2, + 'OAuth2SecurityPolicy_MaxBearerTokenDisclosureAttempts' => 5, + 'OAuth2SecurityPolicy_MaxInvalidClientExceptionAttempts' => 10, + 'OAuth2SecurityPolicy_MaxInvalidRedeemAuthCodeAttempts' => 10, + 'OAuth2SecurityPolicy_MaxInvalidInvalidClientCredentialsAttempts' => 5, +); \ No newline at end of file diff --git a/app/controllers/AdminController.php b/app/controllers/AdminController.php index 5b492e34..b31a8b39 100644 --- a/app/controllers/AdminController.php +++ b/app/controllers/AdminController.php @@ -60,6 +60,7 @@ class AdminController extends BaseController { } $allowed_uris = $client->getClientRegisteredUris(); + $allowed_origins = $client->getClientAllowedOrigins(); $selected_scopes = $client->getClientScopes(); $aux_scopes = array(); @@ -87,6 +88,7 @@ class AdminController extends BaseController { array( 'client' => $client, 'allowed_uris' => $allowed_uris, + 'allowed_origins' => $allowed_origins, 'selected_scopes' => $aux_scopes, 'scopes' => $scopes, 'access_tokens' => $access_tokens, diff --git a/app/controllers/UserController.php b/app/controllers/UserController.php index 6cb87085..2284be82 100644 --- a/app/controllers/UserController.php +++ b/app/controllers/UserController.php @@ -11,7 +11,7 @@ use openid\services\IServerConfigurationService; use openid\services\ITrustedSitesService; use openid\services\IUserService; use openid\XRDS\XRDSDocumentBuilder; -use services\IPHelper; +use utils\IPHelper; use services\IUserActionService; use strategies\DefaultLoginStrategy; use strategies\OAuth2ConsentStrategy; diff --git a/app/controllers/apis/ApiBannedIPController.php b/app/controllers/apis/ApiBannedIPController.php index f2dd733d..0aebce40 100644 --- a/app/controllers/apis/ApiBannedIPController.php +++ b/app/controllers/apis/ApiBannedIPController.php @@ -3,11 +3,18 @@ use utils\services\IBannedIPService; use utils\services\ILogService; +/** + * Class ApiBannedIPController + */ class ApiBannedIPController extends AbstractRESTController implements ICRUDController { private $banned_ip_service; + /** + * @param IBannedIPService $banned_ip_service + * @param ILogService $log_service + */ public function __construct(IBannedIPService $banned_ip_service, ILogService $log_service) { diff --git a/app/controllers/apis/ApiEndpointController.php b/app/controllers/apis/ApiEndpointController.php index c04cf787..7e538ba8 100644 --- a/app/controllers/apis/ApiEndpointController.php +++ b/app/controllers/apis/ApiEndpointController.php @@ -69,8 +69,9 @@ class ApiEndpointController extends AbstractRESTController implements ICRUDContr $rules = array( 'name' => 'required|alpha_dash|max:255', - 'description' => 'required|text', + 'description' => 'required|freetext', 'active' => 'required|boolean', + 'allow_cors' => 'required|boolean', 'route' => 'required|route', 'http_method' => 'required|httpmethod', 'api_id' => 'required|integer', @@ -88,6 +89,7 @@ class ApiEndpointController extends AbstractRESTController implements ICRUDContr $new_api_endpoint['name'], $new_api_endpoint['description'], $new_api_endpoint['active'], + $new_api_endpoint['allow_cors'], $new_api_endpoint['route'], $new_api_endpoint['http_method'], $new_api_endpoint['api_id'] @@ -128,8 +130,9 @@ class ApiEndpointController extends AbstractRESTController implements ICRUDContr $rules = array( 'id' => 'required|integer', 'name' => 'sometimes|required|alpha_dash|max:255', - 'description' => 'sometimes|required|text', + 'description' => 'sometimes|required|freetext', 'active' => 'sometimes|required|boolean', + 'allow_cors' => 'sometimes|required|boolean', 'route' => 'sometimes|required|route', 'http_method' => 'sometimes|required|httpmethod', ); diff --git a/app/controllers/apis/ApiScopeController.php b/app/controllers/apis/ApiScopeController.php index 586233d8..7957aaf3 100644 --- a/app/controllers/apis/ApiScopeController.php +++ b/app/controllers/apis/ApiScopeController.php @@ -68,8 +68,8 @@ class ApiScopeController extends AbstractRESTController implements ICRUDControll $rules = array( 'name' => 'required|scopename|max:512', - 'short_description' => 'required|text|max:512', - 'description' => 'required|text', + 'short_description' => 'required|freetext|max:512', + 'description' => 'required|freetext', 'active' => 'required|boolean', 'default' => 'required|boolean', 'system' => 'required|boolean', @@ -135,8 +135,8 @@ class ApiScopeController extends AbstractRESTController implements ICRUDControll $rules = array( 'id' => 'required|integer', 'name' => 'sometimes|required|scopename|max:512', - 'description' => 'sometimes|required|text', - 'short_description' => 'sometimes|required|text|max:512', + 'description' => 'sometimes|required|freetext', + 'short_description' => 'sometimes|required|freetext|max:512', 'active' => 'sometimes|required|boolean', 'system' => 'sometimes|required|boolean', 'default' => 'sometimes|required|boolean', diff --git a/app/controllers/apis/ClientApiController.php b/app/controllers/apis/ClientApiController.php index 135f31da..f0601df6 100644 --- a/app/controllers/apis/ClientApiController.php +++ b/app/controllers/apis/ClientApiController.php @@ -18,6 +18,7 @@ class ClientApiController extends AbstractRESTController implements ICRUDControl private $scope_service; private $token_service; + /** * @param IApiScopeService $scope_service * @param ITokenService $token_service @@ -65,8 +66,9 @@ class ClientApiController extends AbstractRESTController implements ICRUDControl // Build the validation constraint set. $rules = array( 'user_id' => 'required|integer', - 'application_name' => 'required|alpha_dash|max:255', - 'application_description' => 'required|text', + 'app_name' => 'required|alpha_dash|max:255', + 'app_description' => 'required|freetext', + 'website' => 'required|url', 'application_type' => 'required|applicationtype', ); @@ -78,11 +80,11 @@ class ClientApiController extends AbstractRESTController implements ICRUDControl return $this->error400(array('error'=>'validation','messages' => $messages)); } - if ($this->client_service->existClientAppName($values['application_name'])) { + if ($this->client_service->existClientAppName($values['app_name'])) { return $this->error400(array('error' => 'application Name already exists!.')); } - $new_client = $this->client_service->addClient($values['application_type'], intval($values['user_id']), trim($values['application_name']), trim($values['application_description'])); + $new_client = $this->client_service->addClient($values['application_type'], intval($values['user_id']), trim($values['app_name']), trim($values['app_description']), trim($values['website'])); return $this->created(array('client_id' => $new_client->id)); @@ -150,12 +152,13 @@ class ClientApiController extends AbstractRESTController implements ICRUDControl $values = Input::all(); $rules = array( - 'id' => 'required|integer', - 'app_name' => 'sometimes|required|alpha_dash|max:255', - 'app_description' => 'sometimes|required|text', - 'active' => 'sometimes|required|boolean', - 'locked' => 'sometimes|required|boolean', - 'use_refresh_token' => 'sometimes|required|boolean', + 'id' => 'required|integer', + 'app_name' => 'sometimes|required|alpha_dash|max:255', + 'app_description' => 'sometimes|required|freetext', + 'website' => 'sometimes|required|url', + 'active' => 'sometimes|required|boolean', + 'locked' => 'sometimes|required|boolean', + 'use_refresh_token' => 'sometimes|required|boolean', 'rotate_refresh_token' => 'sometimes|required|boolean', ); @@ -180,6 +183,10 @@ class ClientApiController extends AbstractRESTController implements ICRUDControl } } + /** + * @param $id + * @return mixed + */ public function getRegisteredUris($id) { try { @@ -198,6 +205,10 @@ class ClientApiController extends AbstractRESTController implements ICRUDControl } } + /** + * @param $id + * @return mixed + */ public function addAllowedRedirectUri($id) { try { @@ -210,7 +221,7 @@ class ClientApiController extends AbstractRESTController implements ICRUDControl $validation = Validator::make($values, $rules); if ($validation->fails()) { $messages = $validation->messages()->toArray(); - return $this->error400(array('error' => $messages)); + return $this->error400(array('error'=>'validation','messages' => $messages)); } $res = $this->client_service->addClientAllowedUri($id, $values['redirect_uri']); return $res ? $this->ok(): $this->error404(array('error' => 'operation failed')); @@ -226,6 +237,11 @@ class ClientApiController extends AbstractRESTController implements ICRUDControl } } + /** + * @param $id + * @param $uri_id + * @return mixed + */ public function deleteClientAllowedUri($id, $uri_id) { try { @@ -308,7 +324,7 @@ class ClientApiController extends AbstractRESTController implements ICRUDControl $validation = Validator::make($values, $rules); if ($validation->fails()) { $messages = $validation->messages()->toArray(); - return $this->error400(array('error' => $messages)); + return $this->error400(array('error'=>'validation','messages' => $messages)); } $res = $this->client_service->setRefreshTokenUsage($id, $values['use_refresh_token']); @@ -337,7 +353,7 @@ class ClientApiController extends AbstractRESTController implements ICRUDControl $validation = Validator::make($values, $rules); if ($validation->fails()) { $messages = $validation->messages()->toArray(); - return $this->error400(array('error' => $messages)); + return $this->error400(array('error'=>'validation','messages' => $messages)); } $res = $this->client_service->setRotateRefreshTokenPolicy($id, $values['rotate_refresh_token']); @@ -454,4 +470,75 @@ class ClientApiController extends AbstractRESTController implements ICRUDControl return $this->error500($ex); } } + + + + /** + * @param $id + * @return mixed + */ + public function geAllowedOrigins($id) + { + try { + $client = $this->client_service->getClientByIdentifier($id); + $allowed_origins = $client->allowed_origins()->get(array('id', 'allowed_origin')); + $data = array(); + foreach ($allowed_origins as $origin) { + array_push($data, $origin->toArray()); + } + return $this->ok(array('allowed_origins' => $data)); + } catch (Exception $ex) { + $this->log_service->error($ex); + return $this->error500($ex); + } + } + + /** + * @param $id + * @return mixed + */ + public function addAllowedOrigin($id) + { + try { + $values = Input::All(); + // Build the validation constraint set. + $rules = array( + 'origin' => 'sslorigin|required', + ); + // Creates a Validator instance and validates the data. + $validation = Validator::make($values, $rules); + if ($validation->fails()) { + $messages = $validation->messages()->toArray(); + return $this->error400(array('error'=>'validation','messages' => $messages)); + } + $res = $this->client_service->addClientAllowedOrigin($id, $values['origin']); + return $res ? $this->ok(): $this->error404(array('error' => 'operation failed')); + } catch (AllowedClientUriAlreadyExistsException $ex1) { + $this->log_service->error($ex1); + return $this->error400(array('error' => $ex1->getMessage())); + } catch (AbsentClientException $ex2) { + $this->log_service->error($ex2); + return $this->error404(array('error' => $ex2->getMessage())); + } catch (Exception $ex) { + $this->log_service->error($ex); + return $this->error500($ex); + } + } + + /** + * @param $id + * @param $origin_id + * @return mixed + */ + public function deleteClientAllowedOrigin($id, $origin_id) + { + try { + $res = $this->client_service->deleteClientAllowedOrigin($id, $origin_id); + return $res ? $this->ok() : $this->error404(array('error' => 'operation failed')); + } catch (Exception $ex) { + $this->log_service->error($ex); + return $this->error500($ex); + } + } + } \ No newline at end of file diff --git a/app/controllers/apis/JsonController.php b/app/controllers/apis/JsonController.php index 05a51e46..5063046a 100644 --- a/app/controllers/apis/JsonController.php +++ b/app/controllers/apis/JsonController.php @@ -5,7 +5,7 @@ use utils\services\ILogService; /** * Class JsonController */ -class JsonController extends BaseController { +abstract class JsonController extends BaseController { protected $log_service; diff --git a/app/controllers/apis/UserApiController.php b/app/controllers/apis/UserApiController.php index 842d8929..ff94606a 100644 --- a/app/controllers/apis/UserApiController.php +++ b/app/controllers/apis/UserApiController.php @@ -72,7 +72,17 @@ class UserApiController extends AbstractRESTController implements ICRUDControlle public function get($id) { - // TODO: Implement get() method. + try { + $user = $this->user_service->get($id); + if(is_null($user)){ + return $this->error404(array('error' => 'user not found')); + } + $data = $user->toArray(); + return $this->ok($data); + } catch (Exception $ex) { + $this->log_service->error($ex); + return $this->error500($ex); + } } public function create() diff --git a/app/controllers/apis/protected/OAuth2ProtectedController.php b/app/controllers/apis/protected/OAuth2ProtectedController.php index 5bb7738b..2b6df3f8 100644 --- a/app/controllers/apis/protected/OAuth2ProtectedController.php +++ b/app/controllers/apis/protected/OAuth2ProtectedController.php @@ -7,7 +7,7 @@ use utils\services\ILogService; * Class OAuth2ProtectedController * OAuth2 Protected Base API */ -class OAuth2ProtectedController extends JsonController { +abstract class OAuth2ProtectedController extends JsonController { protected $resource_server_context; diff --git a/app/controllers/apis/protected/OAuth2UserApiController.php b/app/controllers/apis/protected/OAuth2UserApiController.php new file mode 100644 index 00000000..382ae0a3 --- /dev/null +++ b/app/controllers/apis/protected/OAuth2UserApiController.php @@ -0,0 +1,33 @@ +user_service = $user_service; + } + + /** + * Gets User Basic Info + * @return mixed + */ + public function me(){ + try{ + $data = $this->user_service->getCurrentUserInfo(); + return $this->ok($data); + } + catch(Exception $ex){ + $this->log_service->error($ex); + return $this->error500($ex); + } + } +} + diff --git a/app/database/migrations/2013_12_04_182948_create_oauth2_clients_authorized_realm.php b/app/database/migrations/2013_12_04_182948_create_oauth2_clients_authorized_realm.php deleted file mode 100644 index 23d8a7a1..00000000 --- a/app/database/migrations/2013_12_04_182948_create_oauth2_clients_authorized_realm.php +++ /dev/null @@ -1,38 +0,0 @@ -bigIncrements('id')->unsigned(); - $table->string('realm',255); - - $table->bigInteger("client_id")->unsigned(); - $table->index('client_id'); - $table->foreign('client_id')->references('id')->on('oauth2_client') - ->onDelete('cascade') - ->onUpdate('no action'); - - $table->timestamps(); - }); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Schema::table('oauth2_client_authorized_realm', function($table) - { - $table->dropForeign('client_id'); - }); - Schema::dropIfExists('oauth2_client_authorized_realm'); - } - -} \ No newline at end of file diff --git a/app/database/migrations/2014_02_07_141946_create_oauth2_client_allowed_origin.php b/app/database/migrations/2014_02_07_141946_create_oauth2_client_allowed_origin.php new file mode 100644 index 00000000..a6795f9b --- /dev/null +++ b/app/database/migrations/2014_02_07_141946_create_oauth2_client_allowed_origin.php @@ -0,0 +1,45 @@ +bigIncrements('id')->unsigned(); + $table->text('allowed_origin'); + + $table->bigInteger("client_id")->unsigned(); + $table->index('client_id'); + $table->foreign('client_id') + ->references('id') + ->on('oauth2_client') + ->onDelete('cascade') + ->onUpdate('no action'); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('oauth2_client_allowed_origin', function($table) + { + $table->dropForeign('client_id'); + }); + + Schema::dropIfExists('oauth2_client_allowed_origin'); + } +} \ No newline at end of file diff --git a/app/database/migrations/2014_02_03_132103_update_oauth2_client.php b/app/database/migrations/2014_02_07_142007_update_oauth2_client.php similarity index 87% rename from app/database/migrations/2014_02_03_132103_update_oauth2_client.php rename to app/database/migrations/2014_02_07_142007_update_oauth2_client.php index ea64686e..91caa418 100644 --- a/app/database/migrations/2014_02_03_132103_update_oauth2_client.php +++ b/app/database/migrations/2014_02_07_142007_update_oauth2_client.php @@ -13,6 +13,7 @@ class UpdateOauth2Client extends Migration { { Schema::table('oauth2_client', function($table) { + $table->text("website"); $table->enum('application_type', array('WEB_APPLICATION', 'JS_CLIENT','SERVICE')); }); } @@ -24,12 +25,10 @@ class UpdateOauth2Client extends Migration { */ public function down() { - Schema::table('oauth2_client', function($table) { - + $table->dropColumn('website'); $table->dropColumn('application_type'); }); } - } \ No newline at end of file diff --git a/app/database/migrations/2014_02_07_142031_update_oauth2_api_endpoint.php b/app/database/migrations/2014_02_07_142031_update_oauth2_api_endpoint.php new file mode 100644 index 00000000..73ded1e4 --- /dev/null +++ b/app/database/migrations/2014_02_07_142031_update_oauth2_api_endpoint.php @@ -0,0 +1,32 @@ +boolean('allow_cors')->default(true); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('oauth2_api_endpoint', function($table) + { + $table->dropColumn('allow_cors'); + }); + } +} \ No newline at end of file diff --git a/app/database/seeds/ApiEndpointSeeder.php b/app/database/seeds/ApiEndpointSeeder.php index fa1d41fc..57084f2f 100644 --- a/app/database/seeds/ApiEndpointSeeder.php +++ b/app/database/seeds/ApiEndpointSeeder.php @@ -1,457 +1,38 @@ delete(); DB::table('oauth2_api_endpoint')->delete(); - $this->seedResourceServerEndpoints(); - $this->seedApiEndpoints(); - $this->seedApiEndpointEndpoints(); - $this->seedScopeEndpoints(); + $this->seedUsersEndpoints(); } - private function seedResourceServerEndpoints(){ - - $current_realm = Config::get('app.url'); - $resource_server = Api::where('name','=','resource-server')->first(); - - ApiEndpoint::create( - array( - 'name' => 'create-resource-server', - 'active' => true, - 'api_id' => $resource_server->id, - 'route' => 'api/v1/resource-server', - 'http_method' => 'POST' - ) - ); - - ApiEndpoint::create( - array( - 'name' => 'get-resource-server', - 'active' => true, - 'api_id' => $resource_server->id, - 'route' => 'api/v1/resource-server/{id}', - 'http_method' => 'GET' - ) - ); - - ApiEndpoint::create( - array( - 'name' => 'resource-server-regenerate-secret', - 'active' => true, - 'api_id' => $resource_server->id, - 'route' => 'api/v1/resource-server/regenerate-client-secret/{id}', - 'http_method' => 'GET' - ) - ); - - ApiEndpoint::create( - array( - 'name' => 'resource-server-get-page', - 'active' => true, - 'api_id' => $resource_server->id, - 'route' => 'api/v1/resource-server/{page_nbr}/{page_size}', - 'http_method' => 'GET' - ) - ); - - ApiEndpoint::create( - array( - 'name' => 'resource-server-delete', - 'active' => true, - 'api_id' => $resource_server->id, - 'route' => 'api/v1/resource-server/{id}', - 'http_method' => 'DELETE' - ) - ); - - ApiEndpoint::create( - array( - 'name' => 'resource-server-update', - 'active' => true, - 'api_id' => $resource_server->id, - 'route' => 'api/v1/resource-server', - 'http_method' => 'PUT' - ) - ); - - ApiEndpoint::create( - array( - 'name' => 'resource-server-update-status', - 'active' => true, - 'api_id' => $resource_server->id, - 'route' => 'api/v1/resource-server/status/{id}/{active}', - 'http_method' => 'GET' - ) - ); - - //attach scopes to endpoints - - //resource server api scopes - - $resource_server_read_scope = ApiScope::where('name','=',sprintf('%s/resource-server/read',$current_realm))->first(); - $resource_server_write_scope = ApiScope::where('name','=',sprintf('%s/resource-server/write',$current_realm))->first(); - $resource_server_read_page_scope = ApiScope::where('name','=',sprintf('%s/resource-server/read.page',$current_realm))->first(); - $resource_server_regenerate_secret_scope = ApiScope::where('name','=',sprintf('%s/resource-server/regenerate.secret',$current_realm))->first(); - $resource_server_delete_scope = ApiScope::where('name','=',sprintf('%s/resource-server/delete',$current_realm))->first(); - $resource_server_update_scope = ApiScope::where('name','=',sprintf('%s/resource-server/update',$current_realm))->first(); - $resource_server_update_status_scope = ApiScope::where('name','=',sprintf('%s/resource-server/update.status',$current_realm))->first(); - - - // create needs write access - $resource_server_api_create = ApiEndpoint::where('name','=','create-resource-server')->first(); - $resource_server_api_create->scopes()->attach($resource_server_write_scope->id); - - //get needs read access - $resource_server_api_get = ApiEndpoint::where('name','=','get-resource-server')->first(); - $resource_server_api_get->scopes()->attach($resource_server_read_scope->id); - - // get page needs read access or read page access - $resource_server_api_get_page = ApiEndpoint::where('name','=','resource-server-get-page')->first(); - $resource_server_api_get_page->scopes()->attach($resource_server_read_scope->id); - $resource_server_api_get_page->scopes()->attach($resource_server_read_page_scope->id); - - //regenerate secret needs write access or specific access - $resource_server_api_regenerate = ApiEndpoint::where('name','=','resource-server-regenerate-secret')->first(); - $resource_server_api_regenerate->scopes()->attach($resource_server_write_scope->id); - $resource_server_api_regenerate->scopes()->attach($resource_server_regenerate_secret_scope->id); - - //deletes needs delete access - $resource_server_api_delete = ApiEndpoint::where('name','=','resource-server-delete')->first(); - $resource_server_api_delete->scopes()->attach($resource_server_delete_scope->id); - - //update needs update access - $resource_server_api_update = ApiEndpoint::where('name','=','resource-server-update')->first(); - $resource_server_api_update->scopes()->attach($resource_server_update_scope->id); - - //update status needs update access or specific access - $resource_server_api_update_status = ApiEndpoint::where('name','=','resource-server-update-status')->first(); - $resource_server_api_update_status->scopes()->attach($resource_server_update_scope->id); - $resource_server_api_update_status->scopes()->attach($resource_server_update_status_scope->id); - - - } - - private function seedApiEndpoints(){ - - $current_realm = Config::get('app.url'); - $api_api = Api::where('name','=','api')->first(); - - ApiEndpoint::create( - array( - 'name' => 'get-api', - 'active' => true, - 'api_id' => $api_api->id, - 'route' => 'api/v1/api/{id}', - 'http_method' => 'GET' - ) - ); - - - ApiEndpoint::create( - array( - 'name' => 'delete-api', - 'active' => true, - 'api_id' => $api_api->id, - 'route' => 'api/v1/api/{id}', - 'http_method' => 'DELETE' - ) - ); - - ApiEndpoint::create( - array( - 'name' => 'create-api', - 'active' => true, - 'api_id' => $api_api->id, - 'route' => 'api/v1/api', - 'http_method' => 'POST' - ) - ); - - ApiEndpoint::create( - array( - 'name' => 'update-api', - 'active' => true, - 'api_id' => $api_api->id, - 'route' => 'api/v1/api', - 'http_method' => 'PUT' - ) - ); - - ApiEndpoint::create( - array( - 'name' => 'update-api-status', - 'active' => true, - 'api_id' => $api_api->id, - 'route' => 'api/v1/api/status/{id}/{active}', - 'http_method' => 'GET' - ) - ); - - ApiEndpoint::create( - array( - 'name' => 'api-get-page', - 'active' => true, - 'api_id' => $api_api->id, - 'route' => 'api/v1/api/{page_nbr}/{page_size}', - 'http_method' => 'GET' - ) - ); - - //endpoint api scopes - - $api_read_scope = ApiScope::where('name','=',sprintf('%s/api/read',$current_realm))->first(); - $api_write_scope = ApiScope::where('name','=',sprintf('%s/api/write',$current_realm))->first(); - $api_read_page_scope = ApiScope::where('name','=',sprintf('%s/api/read.page',$current_realm))->first(); - $api_delete_scope = ApiScope::where('name','=',sprintf('%s/api/delete',$current_realm))->first(); - $api_update_scope = ApiScope::where('name','=',sprintf('%s/api/update',$current_realm))->first(); - $api_update_status_scope = ApiScope::where('name','=',sprintf('%s/api/update.status',$current_realm))->first(); - - $endpoint_api_get = ApiEndpoint::where('name','=','get-api')->first(); - $endpoint_api_get->scopes()->attach($api_read_scope->id); - - $endpoint_api_get_page = ApiEndpoint::where('name','=','api-get-page')->first(); - $endpoint_api_get_page->scopes()->attach($api_read_scope->id); - $endpoint_api_get_page->scopes()->attach($api_read_page_scope->id); - - $endpoint_api_delete = ApiEndpoint::where('name','=','delete-api')->first(); - $endpoint_api_delete->scopes()->attach($api_delete_scope->id); - - $endpoint_api_create = ApiEndpoint::where('name','=','create-api')->first(); - $endpoint_api_create->scopes()->attach($api_write_scope->id); - - $endpoint_api_update = ApiEndpoint::where('name','=','update-api')->first(); - $endpoint_api_update->scopes()->attach($api_update_scope->id); - - $endpoint_api_update_status = ApiEndpoint::where('name','=','update-api-status')->first(); - $endpoint_api_update_status->scopes()->attach($api_update_scope->id); - $endpoint_api_update_status->scopes()->attach($api_update_status_scope->id); - } - - private function seedApiEndpointEndpoints(){ - - $current_realm = Config::get('app.url'); - $api_api_endpoint = Api::where('name','=','api-endpoint')->first(); - - ApiEndpoint::create( - array( - 'name' => 'get-api-endpoint', - 'active' => true, - 'api_id' => $api_api_endpoint->id, - 'route' => 'api/v1/api-endpoint/{id}', - 'http_method' => 'GET' - ) - ); - - ApiEndpoint::create( - array( - 'name' => 'delete-api-endpoint', - 'active' => true, - 'api_id' => $api_api_endpoint->id, - 'route' => 'api/v1/api-endpoint/{id}', - 'http_method' => 'DELETE' - ) - ); - - ApiEndpoint::create( - array( - 'name' => 'create-api-endpoint', - 'active' => true, - 'api_id' => $api_api_endpoint->id, - 'route' => 'api/v1/api-endpoint', - 'http_method' => 'POST' - ) - ); - - ApiEndpoint::create( - array( - 'name' => 'update-api-endpoint', - 'active' => true, - 'api_id' => $api_api_endpoint->id, - 'route' => 'api/v1/api-endpoint', - 'http_method' => 'PUT' - ) - ); - - ApiEndpoint::create( - array( - 'name' => 'update-api-endpoint-status', - 'active' => true, - 'api_id' => $api_api_endpoint->id, - 'route' => 'api/v1/api-endpoint/status/{id}/{active}', - 'http_method' => 'GET' - ) - ); - - ApiEndpoint::create( - array( - 'name' => 'api-endpoint-get-page', - 'active' => true, - 'api_id' => $api_api_endpoint->id, - 'route' => 'api/v1/api-endpoint/{page_nbr}/{page_size}', - 'http_method' => 'GET' - ) - ); - - - ApiEndpoint::create( - array( - 'name' => 'add-api-endpoint-scope', - 'active' => true, - 'api_id' => $api_api_endpoint->id, - 'route' => 'api/v1/api-endpoint/scope/add/{id}/{scope_id}', - 'http_method' => 'GET' - ) - ); - - ApiEndpoint::create( - array( - 'name' => 'remove-api-endpoint-scope', - 'active' => true, - 'api_id' => $api_api_endpoint->id, - 'route' => 'api/v1/api-endpoint/scope/remove/{id}/{scope_id}', - 'http_method' => 'GET' - ) - ); - - //endpoint api endpoint scopes - - $api_endpoint_read_scope = ApiScope::where('name','=',sprintf('%s/api-endpoint/read',$current_realm))->first(); - $api_endpoint_write_scope = ApiScope::where('name','=',sprintf('%s/api-endpoint/write',$current_realm))->first(); - $api_endpoint_read_page_scope = ApiScope::where('name','=',sprintf('%s/api-endpoint/read.page',$current_realm))->first(); - $api_endpoint_delete_scope = ApiScope::where('name','=',sprintf('%s/api-endpoint/delete',$current_realm))->first(); - $api_endpoint_update_scope = ApiScope::where('name','=',sprintf('%s/api-endpoint/update',$current_realm))->first(); - $api_endpoint_update_status_scope = ApiScope::where('name','=',sprintf('%s/api-endpoint/update.status',$current_realm))->first(); - $api_endpoint_add_scope_scope = ApiScope::where('name','=',sprintf('%s/api-endpoint/add.scope',$current_realm))->first(); - $api_endpoint_remove_scope_scope = ApiScope::where('name','=',sprintf('%s/api-endpoint/remove.scope',$current_realm))->first(); - - $endpoint_api_endpoint_get = ApiEndpoint::where('name','=','get-api-endpoint')->first(); - $endpoint_api_endpoint_get->scopes()->attach($api_endpoint_read_scope->id); - - $endpoint_api_endpoint_get_page = ApiEndpoint::where('name','=','api-endpoint-get-page')->first(); - $endpoint_api_endpoint_get_page->scopes()->attach($api_endpoint_read_scope->id); - $endpoint_api_endpoint_get_page->scopes()->attach($api_endpoint_read_page_scope->id); - - $endpoint_api_endpoint_delete = ApiEndpoint::where('name','=','delete-api-endpoint')->first(); - $endpoint_api_endpoint_delete->scopes()->attach($api_endpoint_delete_scope->id); - - $endpoint_api_endpoint_create = ApiEndpoint::where('name','=','create-api-endpoint')->first(); - $endpoint_api_endpoint_create->scopes()->attach($api_endpoint_write_scope->id); - - $endpoint_api_endpoint_update = ApiEndpoint::where('name','=','update-api-endpoint')->first(); - $endpoint_api_endpoint_update->scopes()->attach($api_endpoint_update_scope->id); - - $endpoint_api_add_api_endpoint_scope = ApiEndpoint::where('name','=','add-api-endpoint-scope')->first(); - $endpoint_api_add_api_endpoint_scope->scopes()->attach($api_endpoint_write_scope->id); - $endpoint_api_add_api_endpoint_scope->scopes()->attach($api_endpoint_add_scope_scope->id); - - $endpoint_api_remove_api_endpoint_scope = ApiEndpoint::where('name','=','remove-api-endpoint-scope')->first(); - $endpoint_api_remove_api_endpoint_scope->scopes()->attach($api_endpoint_write_scope->id); - $endpoint_api_remove_api_endpoint_scope->scopes()->attach($api_endpoint_remove_scope_scope->id); - - - $endpoint_api_endpoint_update_status = ApiEndpoint::where('name','=','update-api-endpoint-status')->first(); - $endpoint_api_endpoint_update_status->scopes()->attach($api_endpoint_update_scope->id); - $endpoint_api_endpoint_update_status->scopes()->attach($api_endpoint_update_status_scope->id); - - } - - private function seedScopeEndpoints(){ - $api_scope = Api::where('name','=','api-scope')->first(); - $current_realm = Config::get('app.url'); + private function seedUsersEndpoints() + { + $users = Api::where('name', '=', 'users')->first(); // endpoints scopes ApiEndpoint::create( array( - 'name' => 'get-scope', - 'active' => true, - 'api_id' => $api_scope->id, - 'route' => 'api/v1/api-scope/{id}', - 'http_method' => 'GET' + 'name' => 'get-user-info', + 'active' => true, + 'api_id' => $users->id, + 'route' => '/api/v1/users/me', + 'http_method' => 'GET' ) ); + $profile_scope = ApiScope::where('name', '=', 'profile')->first(); + $email_scope = ApiScope::where('name', '=', 'email')->first(); + $address_scope = ApiScope::where('name', '=', 'address')->first(); - - ApiEndpoint::create( - array( - 'name' => 'delete-scope', - 'active' => true, - 'api_id' => $api_scope->id, - 'route' => 'api/v1/api-scope/{id}', - 'http_method' => 'DELETE' - ) - ); - - ApiEndpoint::create( - array( - 'name' => 'create-scope', - 'active' => true, - 'api_id' => $api_scope->id, - 'route' => 'api/v1/api-scope', - 'http_method' => 'POST' - ) - ); - - ApiEndpoint::create( - array( - 'name' => 'update-scope', - 'active' => true, - 'api_id' => $api_scope->id, - 'route' => 'api/v1/api-scope', - 'http_method' => 'PUT' - ) - ); - - ApiEndpoint::create( - array( - 'name' => 'update-scope-status', - 'active' => true, - 'api_id' => $api_scope->id, - 'route' => 'api/v1/api-scope/status/{id}/{active}', - 'http_method' => 'GET' - ) - ); - - ApiEndpoint::create( - array( - 'name' => 'scope-get-page', - 'active' => true, - 'api_id' => $api_scope->id, - 'route' => 'api/v1/api-scope/{page_nbr}/{page_size}', - 'http_method' => 'GET' - ) - ); - - $api_scope_read_scope = ApiScope::where('name','=',sprintf('%s/api-scope/read',$current_realm))->first(); - $api_scope_write_scope = ApiScope::where('name','=',sprintf('%s/api-scope/write',$current_realm))->first(); - $api_scope_read_page_scope = ApiScope::where('name','=',sprintf('%s/api-scope/read.page',$current_realm))->first(); - $api_scope_delete_scope = ApiScope::where('name','=',sprintf('%s/api-scope/delete',$current_realm))->first(); - $api_scope_update_scope = ApiScope::where('name','=',sprintf('%s/api-scope/update',$current_realm))->first(); - $api_scope_update_status_scope = ApiScope::where('name','=',sprintf('%s/api-scope/update.status',$current_realm))->first(); - - - $endpoint_api_scope_get = ApiEndpoint::where('name','=','get-scope')->first(); - $endpoint_api_scope_get->scopes()->attach($api_scope_read_scope->id); - - $endpoint_api_scope_get_page = ApiEndpoint::where('name','=','scope-get-page')->first(); - $endpoint_api_scope_get_page->scopes()->attach($api_scope_read_scope->id); - $endpoint_api_scope_get_page->scopes()->attach($api_scope_read_page_scope->id); - - $endpoint_api_scope_delete = ApiEndpoint::where('name','=','delete-scope')->first(); - $endpoint_api_scope_delete->scopes()->attach($api_scope_delete_scope->id); - - $endpoint_api_scope_create = ApiEndpoint::where('name','=','create-scope')->first(); - $endpoint_api_scope_create->scopes()->attach($api_scope_write_scope->id); - - $endpoint_api_scope_update = ApiEndpoint::where('name','=','update-scope')->first(); - $endpoint_api_scope_update->scopes()->attach($api_scope_update_scope->id); - - $endpoint_api_scope_update_status = ApiEndpoint::where('name','=','update-scope-status')->first(); - $endpoint_api_scope_update_status->scopes()->attach($api_scope_update_scope->id); - $endpoint_api_scope_update_status->scopes()->attach($api_scope_update_status_scope->id); + $get_user_info_endpoint = ApiEndpoint::where('name', '=', 'get-user-info')->first(); + $get_user_info_endpoint->scopes()->attach($profile_scope->id); + $get_user_info_endpoint->scopes()->attach($email_scope->id); + $get_user_info_endpoint->scopes()->attach($address_scope->id); } + + } \ No newline at end of file diff --git a/app/database/seeds/ApiScopeSeeder.php b/app/database/seeds/ApiScopeSeeder.php index 0b2849d9..4daf1067 100644 --- a/app/database/seeds/ApiScopeSeeder.php +++ b/app/database/seeds/ApiScopeSeeder.php @@ -8,305 +8,40 @@ class ApiScopeSeeder extends Seeder { DB::table('oauth2_api_endpoint_api_scope')->delete(); DB::table('oauth2_client_api_scope')->delete(); DB::table('oauth2_api_scope')->delete(); - - $this->seedResourceServerScopes(); - $this->seedApiScopes(); - $this->seedApiEndpointScopes(); - $this->seedApiScopeScopes(); + $this->seedUsersScopes(); } - private function seedResourceServerScopes(){ + private function seedUsersScopes(){ - $resource_server = Api::where('name','=','resource-server')->first(); - $current_realm = Config::get('app.url'); + $users = Api::where('name','=','users')->first(); ApiScope::create( array( - 'name' => sprintf('%s/resource-server/read',$current_realm), - 'short_description' => 'Resource Server Read Access', - 'description' => 'Resource Server Read Access', - 'api_id' => $resource_server->id, - 'system' => true, + 'name' => 'profile', + 'short_description' => 'Allows access to your profile info.', + 'description' => 'This scope value requests access to the End-Users default profile Claims, which are: name, family_name, given_name, middle_name, nickname, preferred_username, profile, picture, website, gender, birthdate, zoneinfo, locale, and updated_at.', + 'api_id' => $users->id, + 'system' => false, ) ); ApiScope::create( array( - 'name' => sprintf('%s/resource-server/read.page',$current_realm), - 'short_description' => 'Resource Server Page Read Access', - 'description' => 'Resource Server Page Read Access', - 'api_id' => $resource_server->id, - 'system' => true, + 'name' => 'email', + 'short_description' => 'Allows access to your email info.', + 'description' => 'This scope value requests access to the email and email_verified Claims.', + 'api_id' => $users->id, + 'system' => false, ) ); ApiScope::create( array( - 'name' => sprintf('%s/resource-server/write',$current_realm), - 'short_description' => 'Resource Server Write Access', - 'description' => 'Resource Server Write Access', - 'api_id' => $resource_server->id, - 'system' => true, - ) - ); - - ApiScope::create( - array( - 'name' => sprintf('%s/resource-server/delete',$current_realm), - 'short_description' => 'Resource Server Delete Access', - 'description' => 'Resource Server Delete Access', - 'api_id' => $resource_server->id, - 'system' => true, - ) - ); - - ApiScope::create( - array( - 'name' => sprintf('%s/resource-server/update',$current_realm), - 'short_description' => 'Resource Server Update Access', - 'description' => 'Resource Server Update Access', - 'api_id' => $resource_server->id, - 'system' => true, - ) - ); - - ApiScope::create( - array( - 'name' => sprintf('%s/resource-server/update.status',$current_realm), - 'short_description' => 'Resource Server Update Status', - 'description' => 'Resource Server Update Status', - 'api_id' => $resource_server->id, - 'system' => true, - ) - ); - - ApiScope::create( - array( - 'name' => sprintf('%s/resource-server/regenerate.secret',$current_realm), - 'short_description' => 'Resource Server Regenerate Client Secret', - 'description' => 'Resource Server Regenerate Client Secret', - 'api_id' => $resource_server->id, - 'system' => true, - ) - ); - - } - - private function seedApiScopes(){ - $api = Api::where('name','=','api')->first(); - $current_realm = Config::get('app.url'); - - ApiScope::create( - array( - 'name' => sprintf('%s/api/read',$current_realm), - 'short_description' => 'Get Api', - 'description' => 'Get Api', - 'api_id' => $api->id, - 'system' => true, - ) - ); - - ApiScope::create( - array( - 'name' => sprintf('%s/api/delete',$current_realm), - 'short_description' => 'Deletes Api', - 'description' => 'Deletes Api', - 'api_id' => $api->id, - 'system' => true, - ) - ); - - ApiScope::create( - array( - 'name' => sprintf('%s/api/write',$current_realm), - 'short_description' => 'Create Api', - 'description' => 'Create Api', - 'api_id' => $api->id, - 'system' => true, - ) - ); - - ApiScope::create( - array( - 'name' => sprintf('%s/api/update',$current_realm), - 'short_description' => 'Update Api', - 'description' => 'Update Api', - 'api_id' => $api->id, - 'system' => true, - ) - ); - - ApiScope::create( - array( - 'name' => sprintf('%s/api/update.status',$current_realm), - 'short_description' => 'Update Api Status', - 'description' => 'Update Api Status', - 'api_id' => $api->id, - 'system' => true, - ) - ); - - ApiScope::create( - array( - 'name' => sprintf('%s/api/read.page',$current_realm), - 'short_description' => 'Get Api By Page', - 'description' => 'Get Api By Page', - 'api_id' => $api->id, - 'system' => true, - ) - ); - - } - - private function seedApiEndpointScopes(){ - $api_endpoint = Api::where('name','=','api-endpoint')->first(); - $current_realm = Config::get('app.url'); - - ApiScope::create( - array( - 'name' => sprintf('%s/api-endpoint/read',$current_realm), - 'short_description' => 'Get Api Endpoint', - 'description' => 'Get Api Endpoint', - 'api_id' => $api_endpoint->id, - 'system' => true, - ) - ); - - ApiScope::create( - array( - 'name' => sprintf('%s/api-endpoint/delete',$current_realm), - 'short_description' => 'Deletes Api Endpoint', - 'description' => 'Deletes Api Endpoint', - 'api_id' => $api_endpoint->id, - 'system' => true, - ) - ); - - ApiScope::create( - array( - 'name' => sprintf('%s/api-endpoint/write',$current_realm), - 'short_description' => 'Create Api Endpoint', - 'description' => 'Create Api Endpoint', - 'api_id' => $api_endpoint->id, - 'system' => true, - ) - ); - - ApiScope::create( - array( - 'name' => sprintf('%s/api-endpoint/update',$current_realm), - 'short_description' => 'Update Api Endpoint', - 'description' => 'Update Api Endpoint', - 'api_id' => $api_endpoint->id, - 'system' => true, - ) - ); - - ApiScope::create( - array( - 'name' => sprintf('%s/api-endpoint/update.status',$current_realm), - 'short_description' => 'Update Api Endpoint Status', - 'description' => 'Update Api Endpoint Status', - 'api_id' => $api_endpoint->id, - 'system' => true, - ) - ); - - ApiScope::create( - array( - 'name' => sprintf('%s/api-endpoint/read.page',$current_realm), - 'short_description' => 'Get Api Endpoints By Page', - 'description' => 'Get Api Endpoints By Page', - 'api_id' => $api_endpoint->id, - 'system' => true, - ) - ); - - - ApiScope::create( - array( - 'name' => sprintf('%s/api-endpoint/add.scope',$current_realm), - 'short_description' => 'Add required scope to endpoint', - 'description' => 'Add required scope to endpoint', - 'api_id' => $api_endpoint->id, - 'system' => true, - ) - ); - - - ApiScope::create( - array( - 'name' => sprintf('%s/api-endpoint/remove.scope',$current_realm), - 'short_description' => 'Remove required scope to endpoint', - 'description' => 'Remove required scope to endpoint', - 'api_id' => $api_endpoint->id, - 'system' => true, - ) - ); - - } - - private function seedApiScopeScopes(){ - $current_realm = Config::get('app.url'); - $api_scope = Api::where('name','=','api-scope')->first(); - - ApiScope::create( - array( - 'name' => sprintf('%s/api-scope/read',$current_realm), - 'short_description' => 'Get Api Scope', - 'description' => 'Get Api Scope', - 'api_id' => $api_scope->id, - 'system' => true, - ) - ); - - ApiScope::create( - array( - 'name' => sprintf('%s/api-scope/delete',$current_realm), - 'short_description' => 'Deletes Api Scope', - 'description' => 'Deletes Api Scope', - 'api_id' => $api_scope->id, - 'system' => true, - ) - ); - - ApiScope::create( - array( - 'name' => sprintf('%s/api-scope/write',$current_realm), - 'short_description' => 'Create Api Scope', - 'description' => 'Create Api Scope', - 'api_id' => $api_scope->id, - 'system' => true, - ) - ); - - ApiScope::create( - array( - 'name' => sprintf('%s/api-scope/update',$current_realm), - 'short_description' => 'Update Api Scope', - 'description' => 'Update Api Scope', - 'api_id' => $api_scope->id, - 'system' => true, - ) - ); - - ApiScope::create( - array( - 'name' => sprintf('%s/api-scope/update.status',$current_realm), - 'short_description' => 'Update Api Scope Status', - 'description' => 'Update Api Scope Status', - 'api_id' => $api_scope->id, - 'system' => true, - ) - ); - - ApiScope::create( - array( - 'name' => sprintf('%s/api-scope/read.page',$current_realm), - 'short_description' => 'Get Api Scopes By Page', - 'description' => 'Get Api Scopes By Page', - 'api_id' => $api_scope->id, - 'system' => true, + 'name' => 'address', + 'short_description' => 'Allows access to your Address info.', + 'description' => 'This scope value requests access to the address Claim.', + 'api_id' => $users->id, + 'system' => false, ) ); diff --git a/app/database/seeds/ApiSeeder.php b/app/database/seeds/ApiSeeder.php index fa07feae..96b437f0 100644 --- a/app/database/seeds/ApiSeeder.php +++ b/app/database/seeds/ApiSeeder.php @@ -12,49 +12,13 @@ class ApiSeeder extends Seeder { Api::create( array( - 'name' => 'resource-server', - 'logo' => null, - 'active' => true, - 'Description' => 'Resource Server CRUD operations', - 'resource_server_id' => $resource_server->id, - 'logo' => asset('img/apis/server.png') - ) - ); - - Api::create( - array( - 'name' => 'api', + 'name' => 'users', 'logo' => null, 'active' => true, - 'Description' => 'Api CRUD operations', + 'Description' => 'User Info', 'resource_server_id' => $resource_server->id, 'logo' => asset('img/apis/server.png') ) ); - - - Api::create( - array( - 'name' => 'api-endpoint', - 'logo' => null, - 'active' => true, - 'Description' => 'Api Endpoints CRUD operations', - 'resource_server_id' => $resource_server->id, - 'logo' => asset('img/apis/server.png') - ) - ); - - Api::create( - array( - 'name' => 'api-scope', - 'logo' => null, - 'active' => true, - 'Description' => 'Api Scopes CRUD operations', - 'resource_server_id' => $resource_server->id, - 'logo' => asset('img/apis/server.png') - ) - ); - } - -} \ No newline at end of file +} \ No newline at end of file diff --git a/app/database/seeds/ResourceServerSeeder.php b/app/database/seeds/ResourceServerSeeder.php index cb2240cd..7d017249 100644 --- a/app/database/seeds/ResourceServerSeeder.php +++ b/app/database/seeds/ResourceServerSeeder.php @@ -6,14 +6,15 @@ class ResourceServerSeeder extends Seeder { { DB::table('oauth2_resource_server')->delete(); $current_realm = Config::get('app.url'); + + $res = @parse_url($current_realm); + ResourceServer::create( array( 'friendly_name' => 'openstack id server', - 'host' => $current_realm, + 'host' => $res['host'], 'ip' => '127.0.0.1' ) ); - } - -} \ No newline at end of file +} \ No newline at end of file diff --git a/app/database/seeds/TestSeeder.php b/app/database/seeds/TestSeeder.php index a8f410d4..08f83432 100644 --- a/app/database/seeds/TestSeeder.php +++ b/app/database/seeds/TestSeeder.php @@ -55,11 +55,13 @@ class TestSeeder extends Seeder { $this->seedApiScopes(); $this->seedApiEndpointScopes(); $this->seedApiScopeScopes(); + $this->seedUsersScopes(); //endpoints $this->seedResourceServerEndpoints(); $this->seedApiEndpoints(); $this->seedApiEndpointEndpoints(); $this->seedScopeEndpoints(); + $this->seedUsersEndpoints(); $this->seedTestUsersAndClients(); } @@ -328,7 +330,6 @@ class TestSeeder extends Seeder { ) ); - Client::create( array( 'app_name' => 'oauth2.service', @@ -344,8 +345,6 @@ class TestSeeder extends Seeder { ) ); - - Client::create( array( 'app_name' => 'oauth2_test_app_public', @@ -464,6 +463,17 @@ class TestSeeder extends Seeder { 'logo' => asset('img/apis/server.png') ) ); + + Api::create( + array( + 'name' => 'users', + 'logo' => null, + 'active' => true, + 'Description' => 'User Info', + 'resource_server_id' => $resource_server->id, + 'logo' => asset('img/apis/server.png') + ) + ); } private function seedResourceServerScopes(){ @@ -763,6 +773,42 @@ class TestSeeder extends Seeder { } + private function seedUsersScopes(){ + $current_realm = Config::get('app.url'); + $users = Api::where('name','=','users')->first(); + + ApiScope::create( + array( + 'name' => 'profile', + 'short_description' => 'This scope value requests access to the End-Users default profile Claims', + 'description' => 'This scope value requests access to the End-Users default profile Claims, which are: name, family_name, given_name, middle_name, nickname, preferred_username, profile, picture, website, gender, birthdate, zoneinfo, locale, and updated_at', + 'api_id' => $users->id, + 'system' => false, + ) + ); + + ApiScope::create( + array( + 'name' => 'email', + 'short_description' => 'This scope value requests access to the email and email_verified Claims', + 'description' => 'This scope value requests access to the email and email_verified Claims', + 'api_id' => $users->id, + 'system' => false, + ) + ); + + ApiScope::create( + array( + 'name' => 'address', + 'short_description' => 'This scope value requests access to the address Claim.', + 'description' => 'This scope value requests access to the address Claim.', + 'api_id' => $users->id, + 'system' => false, + ) + ); + + } + private function seedResourceServerEndpoints(){ $current_realm = Config::get('app.url'); @@ -1203,5 +1249,28 @@ class TestSeeder extends Seeder { $endpoint_api_scope_update_status->scopes()->attach($api_scope_update_scope->id); $endpoint_api_scope_update_status->scopes()->attach($api_scope_update_status_scope->id); } -} + private function seedUsersEndpoints(){ + $users = Api::where('name','=','users')->first(); + $current_realm = Config::get('app.url'); + // endpoints scopes + + ApiEndpoint::create( + array( + 'name' => 'get-user-info', + 'active' => true, + 'api_id' => $users->id, + 'route' => 'api/v1/users/me', + 'http_method' => 'GET' + ) + ); + $profile_scope = ApiScope::where('name','=','profile')->first(); + $email_scope = ApiScope::where('name','=','email')->first(); + $address_scope = ApiScope::where('name','=','address')->first(); + + $get_user_info_endpoint = ApiEndpoint::where('name','=','get-user-info')->first(); + $get_user_info_endpoint->scopes()->attach($profile_scope->id); + $get_user_info_endpoint->scopes()->attach($email_scope->id); + $get_user_info_endpoint->scopes()->attach($address_scope->id); + } +} \ No newline at end of file diff --git a/app/filters.php b/app/filters.php index cb338aaf..3e56c6fd 100644 --- a/app/filters.php +++ b/app/filters.php @@ -2,7 +2,7 @@ use openid\exceptions\InvalidOpenIdMessageException; use openid\requests\OpenIdAuthenticationRequest; use openid\services\OpenIdServiceCatalog; -use utils\services\Registry; +use utils\services\ServiceLocator; use utils\services\UtilsServiceCatalog; use oauth2\services\OAuth2ServiceCatalog; use oauth2\exceptions\InvalidAuthorizationRequestException; @@ -17,12 +17,11 @@ use oauth2\exceptions\InvalidAuthorizationRequestException; | */ - //SAP (single access point) -App::before(function ($request) { +App::before(function($request){ try { //checkpoint security pattern entry point - $checkpoint_service = Registry::getInstance()->get(UtilsServiceCatalog::CheckPointService); + $checkpoint_service = ServiceLocator::getInstance()->getService(UtilsServiceCatalog::CheckPointService); if (!$checkpoint_service->check()) { return View::make('404'); } @@ -30,11 +29,19 @@ App::before(function ($request) { Log::error($ex); return View::make('404'); } + + $cors = ServiceLocator::getInstance()->getService('CORSMiddleware'); + if($response = $cors->verifyRequest($request)) + return $response; }); +App::after(function($request, $response){ -App::after(function ($request, $response) { - // + $response->headers->set('X-content-type-options','nosniff'); + $response->headers->set('X-xss-protection','1; mode=block'); + + $cors = ServiceLocator::getInstance()->getService('CORSMiddleware'); + $cors->modifyResponse($request, $response); }); /* @@ -79,7 +86,6 @@ Route::filter('guest', function () { if (Auth::check()) return Redirect::to('/'); }); - /* |-------------------------------------------------------------------------- | CSRF Protection Filter @@ -141,7 +147,7 @@ Route::filter("oauth2.needs.auth.request", function () { Route::filter("ssl", function () { if (!Request::secure()) { - $openid_memento_service = Registry::getInstance()->get(OpenIdServiceCatalog::MementoService); + $openid_memento_service = ServiceLocator::getInstance()->getService(OpenIdServiceCatalog::MementoService); $openid_memento_service->saveCurrentRequest(); $oauth2_memento_service = App::make(OAuth2ServiceCatalog::MementoService); diff --git a/app/filters/OAuth2RequestAccessTokenValidator.php b/app/filters/OAuth2RequestAccessTokenValidator.php index ad513405..5d2c30bc 100644 --- a/app/filters/OAuth2RequestAccessTokenValidator.php +++ b/app/filters/OAuth2RequestAccessTokenValidator.php @@ -18,11 +18,31 @@ use oauth2\IResourceServerContext; */ class OAuth2BearerAccessTokenRequestValidator { + + protected function headers() + { + if (function_exists('getallheaders')) { + // @codeCoverageIgnoreStart + $headers = getallheaders(); + } else { + // @codeCoverageIgnoreEnd + $headers = array(); + foreach ($this->server() as $name => $value) { + if (substr($name, 0, 5) == 'HTTP_') { + $name = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5))))); + $headers[$name] = $value; + } + } + } + return $headers; + } + private $api_endpoint_service; private $token_service; private $log_service; private $checkpoint_service; private $resource_server_context; + private $headers; public function __construct(IResourceServerContext $resource_server_context,IApiEndpointService $api_endpoint_service, ITokenService $token_service, ILogService $log_service, ICheckPointService $checkpoint_service){ $this->api_endpoint_service = $api_endpoint_service; @@ -30,6 +50,7 @@ class OAuth2BearerAccessTokenRequestValidator { $this->log_service = $log_service; $this->checkpoint_service = $checkpoint_service; $this->resource_server_context = $resource_server_context; + $this->headers = $this->headers(); } /** @@ -39,6 +60,10 @@ class OAuth2BearerAccessTokenRequestValidator { public function filter($route, $request) { $url = $route->getPath(); + + if(strpos($url, '/') != 0){ + $url = '/'.$url; + } $method = $request->getMethod(); $realm = $request->getHost(); @@ -51,7 +76,7 @@ class OAuth2BearerAccessTokenRequestValidator { } //check first http basic auth header - $auth_header = Request::header('Authorization'); + $auth_header = isset($this->headers['Authorization'])?$this->headers['Authorization']:null; if(!is_null($auth_header) && !empty($auth_header)) $access_token_value = BearerAccessTokenAuthorizationHeaderParser::getInstance()->parse($auth_header); else{ @@ -94,8 +119,8 @@ class OAuth2BearerAccessTokenRequestValidator { 'scope' => $access_token->getScope() ); - if(!is_null($access_token>getUserId())) - $context['user_id'] = $access_token>getUserId(); + if(!is_null($access_token->getUserId())) + $context['user_id'] = $access_token->getUserId(); $this->resource_server_context->setAuthorizationContext($context); diff --git a/app/lang/en/validation.php b/app/lang/en/validation.php index a7ab6548..23bc2a16 100644 --- a/app/lang/en/validation.php +++ b/app/lang/en/validation.php @@ -103,4 +103,6 @@ return array( 'scopename' => "The :attribute may be a valid scope name.", 'applicationtype' => "The :attribute may be a valid application type.", 'sslurl' => "The :attribute may be a valid URL under ssl schema.", + 'sslorigin' => "The :attribute may be a valid HTTP origin under ssl schema.", + 'freetext' => "The :attribute may only contain text." ); diff --git a/app/libs/auth/AuthenticationServiceProvider.php b/app/libs/auth/AuthenticationServiceProvider.php index c64082d7..f69d1e71 100644 --- a/app/libs/auth/AuthenticationServiceProvider.php +++ b/app/libs/auth/AuthenticationServiceProvider.php @@ -3,7 +3,6 @@ namespace auth; use Illuminate\Support\ServiceProvider; -use utils\services\Registry; use utils\services\UtilsServiceCatalog; class AuthenticationServiceProvider extends ServiceProvider @@ -11,15 +10,16 @@ class AuthenticationServiceProvider extends ServiceProvider public function boot() { - $this->app->singleton(UtilsServiceCatalog::AuthenticationService, 'auth\\AuthService'); - Registry::getInstance()->set(UtilsServiceCatalog::AuthenticationService, $this->app->make(UtilsServiceCatalog::AuthenticationService)); - - $this->app->singleton('auth\\IAuthenticationExtensionService', 'auth\\AuthenticationExtensionService'); - Registry::getInstance()->set('auth\\IAuthenticationExtensionService', $this->app->make('auth\\IAuthenticationExtensionService')); } public function register() { + $this->app->singleton(UtilsServiceCatalog::AuthenticationService, 'auth\\AuthService'); + $this->app->singleton('auth\\IAuthenticationExtensionService', 'auth\\AuthenticationExtensionService'); + } + public function provides() + { + return array('Authentication.services'); } } \ No newline at end of file diff --git a/app/libs/auth/CustomAuthProvider.php b/app/libs/auth/CustomAuthProvider.php index 6320ae84..9d2ceae7 100644 --- a/app/libs/auth/CustomAuthProvider.php +++ b/app/libs/auth/CustomAuthProvider.php @@ -1,7 +1,6 @@ auth_extension_service = $auth_extension_service; + $this->user_service = $user_service; + $this->checkpoint_service = $checkpoint_service; } /** @@ -98,13 +102,11 @@ class CustomAuthProvider implements UserProviderInterface $user = User::where('external_id', '=', $identifier)->first(); } - $user_service = Registry::getInstance()->get(OpenIdServiceCatalog::UserService); + $user_name = $member->FirstName . "." . $member->Surname; //do association between user and member - $user_service->associateUser($user->id, strtolower($user_name)); - - $server_configuration = Registry::getInstance()->get(UtilsServiceCatalog::ServerConfigurationService); + $this->user_service->associateUser($user->id, strtolower($user_name)); //update user fields $user->last_login_date = gmdate("Y-m-d H:i:s", time()); @@ -124,8 +126,7 @@ class CustomAuthProvider implements UserProviderInterface } }); } catch (Exception $ex) { - $checkpoint_service = Registry::getInstance()->get(UtilsServiceCatalog::CheckPointService); - $checkpoint_service->trackException($ex); + $this->checkpoint_service->trackException($ex); Log::error($ex); $user = null; } diff --git a/app/libs/auth/User.php b/app/libs/auth/User.php index ef87afe1..68165ccc 100644 --- a/app/libs/auth/User.php +++ b/app/libs/auth/User.php @@ -7,7 +7,7 @@ use Member; use MemberPhoto; use openid\model\IOpenIdUser; use openid\services\OpenIdServiceCatalog; -use utils\services\Registry; +use utils\services\ServiceLocator; use oauth2\models\IOAuth2User; use Eloquent; use utils\model\BaseModelEloquent; @@ -126,7 +126,7 @@ class User extends BaseModelEloquent implements UserInterface, IOpenIdUser, IOAu public function getNickName() { - return $this->getFullName; + return $this->getFullName(); } public function getGender() @@ -134,7 +134,7 @@ class User extends BaseModelEloquent implements UserInterface, IOpenIdUser, IOAu if (is_null($this->member)) { $this->member = Member::where('Email', '=', $this->external_id)->first(); } - return ""; + return $this->member->Gender; } public function getCountry() @@ -214,7 +214,7 @@ class User extends BaseModelEloquent implements UserInterface, IOpenIdUser, IOAu if (!is_null($photoId) && is_numeric($photoId) && $photoId > 0) { $photo = MemberPhoto::where('ID', '=', $photoId)->first(); if(!is_null($photo)){ - $server_configuration_service = Registry::getInstance()->get(OpenIdServiceCatalog::ServerConfigurationService); + $server_configuration_service = ServiceLocator::getInstance()->getService(OpenIdServiceCatalog::ServerConfigurationService); $url = $server_configuration_service->getConfigValue("Assets.Url").$photo->Filename; } } @@ -262,4 +262,36 @@ class User extends BaseModelEloquent implements UserInterface, IOpenIdUser, IOAu $group = $this->member->groups()->where('code','=',IOpenIdUser::OpenstackIdServerAdminGroup)->first(); return !is_null($group); } + + public function getStreetAddress() + { + if (is_null($this->member)) { + $this->member = Member::where('Email', '=', $this->external_id)->first(); + } + return sprintf("%s, %s ",$this->member->Address,$this->member->Suburb); + } + + public function getRegion() + { + if (is_null($this->member)) { + $this->member = Member::where('Email', '=', $this->external_id)->first(); + } + return $this->member->State; + } + + public function getLocality() + { + if (is_null($this->member)) { + $this->member = Member::where('Email', '=', $this->external_id)->first(); + } + return $this->member->City; + } + + public function getPostalCode() + { + if (is_null($this->member)) { + $this->member = Member::where('Email', '=', $this->external_id)->first(); + } + return $this->member->Postcode; + } } \ No newline at end of file diff --git a/app/libs/oauth2/IResourceServerContext.php b/app/libs/oauth2/IResourceServerContext.php index 3d8346dd..59c9a3d2 100644 --- a/app/libs/oauth2/IResourceServerContext.php +++ b/app/libs/oauth2/IResourceServerContext.php @@ -2,13 +2,42 @@ namespace oauth2; - +/** + * Interface IResourceServerContext + * Current Request OAUTH2 security context + * @package oauth2 + */ interface IResourceServerContext { + /** + * returns given scopes for current requewt + * @return array + */ public function getCurrentScope(); + + /** + * gets current access token valaue + * @return string + */ public function getCurrentAccessToken(); + + /** + * gets current access token lifetime + * @return mixed + */ public function getCurrentAccessTokenLifetime(); + + /** + * gets current client id + * @return string + */ public function getCurrentClientId(); - public function setAuthorizationContext($auth_context); + + /** + * gets current user id (if was set) + * @return int + */ public function getCurrentUserId(); + + public function setAuthorizationContext($auth_context); } \ No newline at end of file diff --git a/app/libs/oauth2/OAuth2Protocol.php b/app/libs/oauth2/OAuth2Protocol.php index 8eafe556..46624679 100644 --- a/app/libs/oauth2/OAuth2Protocol.php +++ b/app/libs/oauth2/OAuth2Protocol.php @@ -31,6 +31,7 @@ use oauth2\exceptions\UnsupportedResponseTypeException; use oauth2\exceptions\UriNotAllowedException; use oauth2\exceptions\MissingClientAuthorizationInfo; use oauth2\exceptions\InvalidRedeemAuthCodeException; +use oauth2\exceptions\InvalidClientCredentials; //grant types use oauth2\grant_types\AuthorizationCodeGrantType; @@ -427,6 +428,11 @@ class OAuth2Protocol implements IOAuth2Protocol $this->checkpoint_service->trackException($ex17); return new OAuth2DirectErrorResponse(OAuth2Protocol::OAuth2Protocol_Error_UnauthorizedClient); } + catch(InvalidClientCredentials $ex18){ + $this->log_service->error($ex18); + $this->checkpoint_service->trackException($ex18); + return new OAuth2DirectErrorResponse(OAuth2Protocol::OAuth2Protocol_Error_UnauthorizedClient); + } catch (Exception $ex) { $this->log_service->error($ex); $this->checkpoint_service->trackException($ex); @@ -478,6 +484,11 @@ class OAuth2Protocol implements IOAuth2Protocol $this->checkpoint_service->trackException($ex2); return new OAuth2DirectErrorResponse(OAuth2Protocol::OAuth2Protocol_Error_InvalidGrant); } + catch(InvalidClientCredentials $ex3){ + $this->log_service->error($ex3); + $this->checkpoint_service->trackException($ex3); + return new OAuth2DirectErrorResponse(OAuth2Protocol::OAuth2Protocol_Error_UnauthorizedClient); + } catch (Exception $ex) { $this->log_service->error($ex); $this->checkpoint_service->trackException($ex); diff --git a/app/libs/oauth2/OAuth2ServiceProvider.php b/app/libs/oauth2/OAuth2ServiceProvider.php index 622ec836..7b34a56b 100644 --- a/app/libs/oauth2/OAuth2ServiceProvider.php +++ b/app/libs/oauth2/OAuth2ServiceProvider.php @@ -3,17 +3,20 @@ namespace oauth2; use Illuminate\Support\ServiceProvider; -use utils\services\Registry; -class OAuth2ServiceProvider extends ServiceProvider +class OAuth2ServiceProvider extends ServiceProvider { public function boot() { - Registry::getInstance()->set('oauth2\IOAuth2Protocol', $this->app->make('oauth2\IOAuth2Protocol')); } public function register() { - $this->app->bind('oauth2\IOAuth2Protocol', 'oauth2\OAuth2Protocol'); + $this->app->singleton('oauth2\IOAuth2Protocol', 'oauth2\OAuth2Protocol'); + } + + public function provides() + { + return array('oauth2'); } } \ No newline at end of file diff --git a/app/libs/oauth2/exceptions/InvalidClientCredentials.php b/app/libs/oauth2/exceptions/InvalidClientCredentials.php new file mode 100644 index 00000000..9ee95a41 --- /dev/null +++ b/app/libs/oauth2/exceptions/InvalidClientCredentials.php @@ -0,0 +1,12 @@ +current_client->getClientType() == IClient::ClientType_Confidential && $this->current_client->getClientSecret() !== $this->current_client_secret) - throw new InvalidClientType($this->current_client_id,sprintf('client id %s',$this->current_client_id)); + throw new InvalidClientCredentials($this->current_client_id, sprintf('client id %s',$this->current_client_id)); } } \ No newline at end of file diff --git a/app/libs/oauth2/grant_types/ValidateBearerTokenGrantType.php b/app/libs/oauth2/grant_types/ValidateBearerTokenGrantType.php index 97d1de17..1032c932 100644 --- a/app/libs/oauth2/grant_types/ValidateBearerTokenGrantType.php +++ b/app/libs/oauth2/grant_types/ValidateBearerTokenGrantType.php @@ -13,7 +13,7 @@ use oauth2\requests\OAuth2Request; use oauth2\responses\OAuth2AccessTokenValidationResponse; use oauth2\services\IClientService; use oauth2\services\ITokenService; -use services\IPHelper; +use utils\IPHelper; use utils\services\ILogService; use oauth2\models\IClient; @@ -126,7 +126,7 @@ class ValidateBearerTokenGrantType extends AbstractGrantType throw new BearerTokenDisclosureAttemptException($this->current_client_id,sprintf('access token current audience does not match with current request ip %s', $current_ip)); } - return new OAuth2AccessTokenValidationResponse($token_value, $access_token->getScope(), $access_token->getAudience(),$access_token->getClientId(),$access_token->getRemainingLifetime(),$access_token->getUserId()); + return new OAuth2AccessTokenValidationResponse( $token_value, $access_token->getScope(), $access_token->getAudience(), $access_token->getClientId(), $access_token->getRemainingLifetime(), $access_token->getUserId()); } catch(InvalidAccessTokenException $ex1){ $this->log_service->error($ex1); diff --git a/app/libs/oauth2/models/AuthorizationCode.php b/app/libs/oauth2/models/AuthorizationCode.php index 0be2882d..e323684c 100644 --- a/app/libs/oauth2/models/AuthorizationCode.php +++ b/app/libs/oauth2/models/AuthorizationCode.php @@ -2,7 +2,7 @@ namespace oauth2\models; -use services\IPHelper; +use utils\IPHelper; use Zend\Math\Rand; use oauth2\OAuth2Protocol; /** diff --git a/app/libs/oauth2/models/IApiEndpoint.php b/app/libs/oauth2/models/IApiEndpoint.php index 6f84ab0f..b642c173 100644 --- a/app/libs/oauth2/models/IApiEndpoint.php +++ b/app/libs/oauth2/models/IApiEndpoint.php @@ -16,6 +16,11 @@ interface IApiEndpoint { public function isActive(); public function setStatus($active); + /** + * @return booll + */ + public function supportCORS(); + /** * @return IApi */ diff --git a/app/libs/oauth2/models/IClient.php b/app/libs/oauth2/models/IClient.php index b496f5bf..fe8a743d 100644 --- a/app/libs/oauth2/models/IClient.php +++ b/app/libs/oauth2/models/IClient.php @@ -15,25 +15,126 @@ interface IClient { const ApplicationType_JS_Client = 'JS_CLIENT'; const ApplicationType_Service = 'SERVICE'; + /** + * @return int + */ public function getId(); + + /** + * @return string + */ public function getClientId(); + + /** + * @return null|string + */ public function getClientSecret(); + + /** + * @return string + */ public function getClientType(); + + /** + * @return string + */ public function getApplicationType(); - public function getClientAuthorizedRealms(); + + /** + * @return mixed + */ public function getClientScopes(); - public function getClientRegisteredUris(); + + /** + * @param $scope + * @return bool + */ public function isScopeAllowed($scope); - public function isRealmAllowed($realm); + + /** + * @return mixed + */ + public function getClientRegisteredUris(); + + /** + * @param $uri + * @return bool + */ public function isUriAllowed($uri); + + /** + * returns all registered allowed js origins for this client + * @return mixed + */ + public function getClientAllowedOrigins(); + + /** + * @param $origin + * @return bool + */ + public function isOriginAllowed($origin); + + /** + * gets application name + * @return string + */ public function getApplicationName(); + + /** gets application log url + * @return string + */ public function getApplicationLogo(); + + /** + * gets application description + * @return string + */ public function getApplicationDescription(); + + /** + * gets application developer email + * @return string + */ public function getDeveloperEmail(); + + /** + * gets user id that owns this application + * @return int + */ public function getUserId(); + + /** + * + * @return bool + */ public function isLocked(); + + /** + * @return bool + */ public function isActive(); + + /** + * clients could be associated to resource server in order + * to do server to server communication + * @return bool + */ public function isResourceServerClient(); + + /** + * gets associated resource server + * @return null|IResourceServer + */ public function getResourceServer(); + + /** + * @return string + */ public function getFriendlyApplicationType(); + + /** + * gets application website url + * @return string + */ + public function getWebsite(); } \ No newline at end of file diff --git a/app/libs/oauth2/models/RefreshToken.php b/app/libs/oauth2/models/RefreshToken.php index 8fbfa60a..a7089cea 100644 --- a/app/libs/oauth2/models/RefreshToken.php +++ b/app/libs/oauth2/models/RefreshToken.php @@ -3,7 +3,7 @@ namespace oauth2\models; use Zend\Math\Rand; -use services\IPHelper; +use utils\IPHelper; use oauth2\OAuth2Protocol; /** * Class RefreshToken diff --git a/app/libs/oauth2/resource_server/IUserService.php b/app/libs/oauth2/resource_server/IUserService.php new file mode 100644 index 00000000..640c80e0 --- /dev/null +++ b/app/libs/oauth2/resource_server/IUserService.php @@ -0,0 +1,26 @@ +log_service = $log_service; + $this->resource_server_context = $resource_server_context; + } +} \ No newline at end of file diff --git a/app/libs/oauth2/responses/OAuth2AccessTokenValidationResponse.php b/app/libs/oauth2/responses/OAuth2AccessTokenValidationResponse.php index bd533909..a0c01322 100644 --- a/app/libs/oauth2/responses/OAuth2AccessTokenValidationResponse.php +++ b/app/libs/oauth2/responses/OAuth2AccessTokenValidationResponse.php @@ -17,6 +17,7 @@ class OAuth2AccessTokenValidationResponse extends OAuth2DirectResponse { $this[OAuth2Protocol::OAuth2Protocol_Scope] = $scope; $this[OAuth2Protocol::OAuth2Protocol_Audience] = $audience; $this[OAuth2Protocol::OAuth2Protocol_AccessToken_ExpiresIn] = $expires_in; + if(!is_null($user_id)){ $this[OAuth2Protocol::OAuth2Protocol_UserId] = $user_id; } diff --git a/app/libs/oauth2/services/IAllowedOriginService.php b/app/libs/oauth2/services/IAllowedOriginService.php new file mode 100644 index 00000000..6c6094bb --- /dev/null +++ b/app/libs/oauth2/services/IAllowedOriginService.php @@ -0,0 +1,17 @@ +get(OAuth2IndirectResponse::OAuth2IndirectResponse); + return ServiceLocator::getInstance()->getService(OAuth2IndirectResponse::OAuth2IndirectResponse); } break; case OAuth2IndirectFragmentResponse::OAuth2IndirectFragmentResponse: { - return Registry::getInstance()->get(OAuth2IndirectFragmentResponse::OAuth2IndirectFragmentResponse); + return ServiceLocator::getInstance()->getService(OAuth2IndirectFragmentResponse::OAuth2IndirectFragmentResponse); } break; case OAuth2DirectResponse::OAuth2DirectResponse: { - return Registry::getInstance()->get(OAuth2DirectResponse::OAuth2DirectResponse); + return ServiceLocator::getInstance()->getService(OAuth2DirectResponse::OAuth2DirectResponse); } break; default: diff --git a/app/libs/openid/OpenIdProtocol.php b/app/libs/openid/OpenIdProtocol.php index 95d3cf88..49e05720 100644 --- a/app/libs/openid/OpenIdProtocol.php +++ b/app/libs/openid/OpenIdProtocol.php @@ -5,10 +5,9 @@ namespace openid; use openid\handlers\OpenIdAuthenticationRequestHandler; use openid\handlers\OpenIdCheckAuthenticationRequestHandler; use openid\handlers\OpenIdSessionAssociationRequestHandler; -use openid\services\OpenIdServiceCatalog; use openid\XRDS\XRDSDocumentBuilder; use openid\XRDS\XRDSService; -use utils\services\Registry; + //services use utils\services\ILogService; use openid\services\IMementoOpenIdRequestService; @@ -22,6 +21,7 @@ use utils\services\IAuthService; use utils\services\ICheckPointService; + /** * Class OpenIdProtocol * OpenId Protocol Implementation @@ -126,7 +126,8 @@ class OpenIdProtocol implements IOpenIdProtocol ); private $request_handlers; - + private $server_extension_service; + private $server_config_service; public function __construct( IAuthService $auth_service, @@ -141,9 +142,11 @@ class OpenIdProtocol implements IOpenIdProtocol ICheckPointService $checkpoint_service) { //create chain of responsibility - $check_auth = new OpenIdCheckAuthenticationRequestHandler($association_service, $nonce_service, $log_service,$checkpoint_service, null); - $session_assoc = new OpenIdSessionAssociationRequestHandler($log_service,$checkpoint_service, $check_auth); - $this->request_handlers = new OpenIdAuthenticationRequestHandler($auth_service, $memento_request_service, $auth_strategy, $server_extension_service, $association_service, $trusted_sites_service, $server_config_service, $nonce_service, $log_service,$checkpoint_service, $session_assoc); + $check_auth = new OpenIdCheckAuthenticationRequestHandler($association_service, $nonce_service, $log_service,$checkpoint_service, null); + $session_assoc = new OpenIdSessionAssociationRequestHandler($log_service,$checkpoint_service, $check_auth); + $this->request_handlers = new OpenIdAuthenticationRequestHandler($auth_service, $memento_request_service, $auth_strategy, $server_extension_service, $association_service, $trusted_sites_service, $server_config_service, $nonce_service, $log_service,$checkpoint_service, $session_assoc); + $this->server_extension_service = $server_extension_service; + $this->server_config_service = $server_config_service; } public static function isAssocTypeSupported($assoc_type) @@ -173,17 +176,13 @@ class OpenIdProtocol implements IOpenIdProtocol public function getXRDSDiscovery($mode, $canonical_id = null) { - $server_extension_service = Registry::getInstance()->get(OpenIdServiceCatalog::ServerExtensionsService); - $server_config_service = Registry::getInstance()->get(OpenIdServiceCatalog::ServerConfigurationService); - - $active_extensions = $server_extension_service->getAllActiveExtensions(); + $active_extensions = $this->server_extension_service->getAllActiveExtensions(); $extensions = array(); foreach ($active_extensions as $ext) { array_push($extensions, $ext->getNamespace()); } - $services = array(); - array_push($services, new XRDSService(0, $mode == IOpenIdProtocol::OpenIdXRDSModeUser ? self::ClaimedIdentifierType : self::OPIdentifierType, $server_config_service->getOPEndpointURL(), $extensions, $canonical_id)); + array_push($services, new XRDSService(0, $mode == IOpenIdProtocol::OpenIdXRDSModeUser ? self::ClaimedIdentifierType : self::OPIdentifierType, $this->server_config_service->getOPEndpointURL(), $extensions, $canonical_id)); $builder = new XRDSDocumentBuilder($services, $canonical_id); $xrds = $builder->render(); return $xrds; diff --git a/app/libs/openid/OpenIdServiceProvider.php b/app/libs/openid/OpenIdServiceProvider.php index 956c6eac..250549bb 100644 --- a/app/libs/openid/OpenIdServiceProvider.php +++ b/app/libs/openid/OpenIdServiceProvider.php @@ -15,9 +15,20 @@ use utils\services\UtilsServiceCatalog; class OpenIdServiceProvider extends ServiceProvider { + public function boot() { - $this->app->bind('openid\IOpenIdProtocol', 'openid\OpenIdProtocol'); + + } + + /** + * Register the service provider. + * + * @return void + */ + public function register() + { + $this->app->singleton('openid\IOpenIdProtocol', 'openid\OpenIdProtocol'); $auth_extension_service = $this->app->make('auth\\IAuthenticationExtensionService'); @@ -28,13 +39,8 @@ class OpenIdServiceProvider extends ServiceProvider } } - /** - * Register the service provider. - * - * @return void - */ - public function register() + public function provides() { - // TODO: Implement register() method. + return array('openid'); } } \ No newline at end of file diff --git a/app/libs/openid/extensions/OpenIdExtension.php b/app/libs/openid/extensions/OpenIdExtension.php index 4a335a56..6d110906 100644 --- a/app/libs/openid/extensions/OpenIdExtension.php +++ b/app/libs/openid/extensions/OpenIdExtension.php @@ -1,11 +1,4 @@ namespace = $namespace; $this->name = $name; $this->view = $view; $this->description = $description; - $this->log_service = Registry::getInstance()->get(UtilsServiceCatalog::LogService); + $this->log_service = $log_service; } public function getNamespace() diff --git a/app/libs/openid/extensions/implementations/OpenIdAXExtension.php b/app/libs/openid/extensions/implementations/OpenIdAXExtension.php index 070329fe..7c5b38d8 100644 --- a/app/libs/openid/extensions/implementations/OpenIdAXExtension.php +++ b/app/libs/openid/extensions/implementations/OpenIdAXExtension.php @@ -10,9 +10,9 @@ use openid\requests\contexts\RequestContext; use openid\requests\OpenIdRequest; use openid\responses\contexts\ResponseContext; use openid\responses\OpenIdResponse; -use utils\services\Registry; +use utils\services\ServiceLocator; use utils\services\UtilsServiceCatalog; - +use utils\services\ILogService; /** * Class OpenIdAXExtension * Implements @@ -36,9 +36,9 @@ class OpenIdAXExtension extends OpenIdExtension const FetchRequest = "fetch_request"; public static $available_properties; - public function __construct($name, $namespace, $view, $description) + public function __construct($name, $namespace, $view, $description, ILogService $log_service) { - parent::__construct($name, $namespace, $view, $description); + parent::__construct($name, $namespace, $view, $description, $log_service); self::$available_properties[OpenIdAXExtension::Country] = "http://axschema.org/contact/country/home"; self::$available_properties[OpenIdAXExtension::Email] = "http://axschema.org/contact/email"; self::$available_properties[OpenIdAXExtension::FirstMame] = "http://axschema.org/namePerson/first"; @@ -72,9 +72,11 @@ class OpenIdAXExtension extends OpenIdExtension $response->addParam(self::paramNamespace(), self::NamespaceUrl); $response->addParam(self::param(self::Mode), self::FetchResponse); $context->addSignParam(self::param(self::Mode)); - $attributes = $ax_request->getRequiredAttributes(); - $auth_service = Registry::getInstance()->get(UtilsServiceCatalog::AuthenticationService); - $user = $auth_service->getCurrentUser(); + + $attributes = $ax_request->getRequiredAttributes(); + $auth_service = ServiceLocator::getInstance()->getService(UtilsServiceCatalog::AuthenticationService); + $user = $auth_service->getCurrentUser(); + foreach ($attributes as $attr) { $response->addParam(self::param(self::Type) . "." . $attr, self::$available_properties[$attr]); $context->addSignParam(self::param(self::Type) . "." . $attr); diff --git a/app/libs/openid/extensions/implementations/OpenIdAXRequest.php b/app/libs/openid/extensions/implementations/OpenIdAXRequest.php index 8ca6c4b9..6351ba7d 100644 --- a/app/libs/openid/extensions/implementations/OpenIdAXRequest.php +++ b/app/libs/openid/extensions/implementations/OpenIdAXRequest.php @@ -6,8 +6,6 @@ use openid\exceptions\InvalidOpenIdMessageException; use openid\helpers\OpenIdErrorMessages; use openid\OpenIdMessage; use openid\requests\OpenIdRequest; - - /** * Class OpenIdAXRequest * Implements http://openid.net/specs/openid-attribute-exchange-1_0.html diff --git a/app/libs/openid/extensions/implementations/OpenIdOAuth2Extension.php b/app/libs/openid/extensions/implementations/OpenIdOAuth2Extension.php index 716d8556..2521e900 100644 --- a/app/libs/openid/extensions/implementations/OpenIdOAuth2Extension.php +++ b/app/libs/openid/extensions/implementations/OpenIdOAuth2Extension.php @@ -12,8 +12,9 @@ use openid\responses\contexts\ResponseContext; use openid\responses\OpenIdResponse; use Exception; -use utils\services\Registry; +use utils\services\ServiceLocator; use utils\services\UtilsServiceCatalog; +use utils\services\ILogService; use oauth2\requests\OAuth2AuthorizationRequest; use oauth2\OAuth2Protocol; @@ -53,14 +54,14 @@ class OpenIdOAuth2Extension extends OpenIdExtension * @param $view * @param $description */ - public function __construct($name, $namespace, $view, $description) + public function __construct($name, $namespace, $view, $description, ILogService $log_service) { - parent::__construct($name, $namespace, $view, $description); + parent::__construct($name, $namespace, $view, $description,$log_service); - $this->oauth2_protocol = Registry::getInstance()->get('oauth2\IOAuth2Protocol'); - $this->checkpoint_service = Registry::getInstance()->get(UtilsServiceCatalog::CheckPointService); - $this->client_service = Registry::getInstance()->get(OAuth2ServiceCatalog::ClientService); - $this->scope_service = Registry::getInstance()->get(OAuth2ServiceCatalog::ScopeService); + $this->oauth2_protocol = ServiceLocator::getInstance()->getService('oauth2\IOAuth2Protocol'); + $this->checkpoint_service = ServiceLocator::getInstance()->getService(UtilsServiceCatalog::CheckPointService); + $this->client_service = ServiceLocator::getInstance()->getService(OAuth2ServiceCatalog::ClientService); + $this->scope_service = ServiceLocator::getInstance()->getService(OAuth2ServiceCatalog::ScopeService); } /** @@ -136,6 +137,7 @@ class OpenIdOAuth2Extension extends OpenIdExtension 'app_name' => $client->getApplicationName(), 'app_logo' => $client->getApplicationLogo(), 'redirect_to' => $return_to, + 'website' => $client->getWebsite(), 'dev_info_email' => $client->getDeveloperEmail() )); diff --git a/app/libs/openid/extensions/implementations/OpenIdPAPEExtension.php b/app/libs/openid/extensions/implementations/OpenIdPAPEExtension.php index 91154bed..8232b773 100644 --- a/app/libs/openid/extensions/implementations/OpenIdPAPEExtension.php +++ b/app/libs/openid/extensions/implementations/OpenIdPAPEExtension.php @@ -1,12 +1,4 @@ getOptionalAttributes(); $attributes = array_merge($attributes, $opt_attributes); - $auth_service = Registry::getInstance()->get(UtilsServiceCatalog::AuthenticationService); + $auth_service = ServiceLocator::getInstance()->getService(UtilsServiceCatalog::AuthenticationService); $user = $auth_service->getCurrentUser(); foreach ($attributes as $attr => $value) { diff --git a/app/libs/openid/extensions/implementations/OpenIdSREGRequest.php b/app/libs/openid/extensions/implementations/OpenIdSREGRequest.php index 034b01f2..ac842060 100644 --- a/app/libs/openid/extensions/implementations/OpenIdSREGRequest.php +++ b/app/libs/openid/extensions/implementations/OpenIdSREGRequest.php @@ -1,17 +1,11 @@ attributes = array(); $this->optional_attributes = array(); - $this->log = Registry::getInstance()->get(UtilsServiceCatalog::LogService); } public function isValid() @@ -83,7 +75,7 @@ class OpenIdSREGRequest extends OpenIdRequest return true; } } catch (Exception $ex) { - $this->log->error($ex); + $this->log_service->error($ex); } return false; } diff --git a/app/libs/openid/handlers/strategies/session_association/implementations/SessionAssociationDHStrategy.php b/app/libs/openid/handlers/strategies/session_association/implementations/SessionAssociationDHStrategy.php index d9b2770e..1c6f3517 100644 --- a/app/libs/openid/handlers/strategies/session_association/implementations/SessionAssociationDHStrategy.php +++ b/app/libs/openid/handlers/strategies/session_association/implementations/SessionAssociationDHStrategy.php @@ -9,7 +9,7 @@ use openid\model\IAssociation; use openid\requests\OpenIdDHAssociationSessionRequest; use openid\responses\OpenIdDiffieHellmanAssociationSessionResponse; use openid\services\OpenIdServiceCatalog; -use utils\services\Registry; +use utils\services\ServiceLocator; use utils\services\UtilsServiceCatalog; use Zend\Crypt\PublicKey\DiffieHellman; @@ -27,9 +27,9 @@ class SessionAssociationDHStrategy implements ISessionAssociationStrategy public function __construct(OpenIdDHAssociationSessionRequest $request) { $this->current_request = $request; - $this->association_service = Registry::getInstance()->get(OpenIdServiceCatalog::AssociationService); - $this->server_configuration_service = Registry::getInstance()->get(OpenIdServiceCatalog:: ServerConfigurationService); - $this->log = Registry::getInstance()->get(UtilsServiceCatalog:: LogService); + $this->association_service = ServiceLocator::getInstance()->getService(OpenIdServiceCatalog::AssociationService); + $this->server_configuration_service = ServiceLocator::getInstance()->getService(OpenIdServiceCatalog:: ServerConfigurationService); + $this->log = ServiceLocator::getInstance()->getService(UtilsServiceCatalog:: LogService); } /** diff --git a/app/libs/openid/handlers/strategies/session_association/implementations/SessionAssociationUnencryptedStrategy.php b/app/libs/openid/handlers/strategies/session_association/implementations/SessionAssociationUnencryptedStrategy.php index 1fde40dc..7bce66d1 100644 --- a/app/libs/openid/handlers/strategies/session_association/implementations/SessionAssociationUnencryptedStrategy.php +++ b/app/libs/openid/handlers/strategies/session_association/implementations/SessionAssociationUnencryptedStrategy.php @@ -11,7 +11,7 @@ use openid\requests\OpenIdAssociationSessionRequest; use openid\responses\OpenIdAssociationSessionResponse; use openid\responses\OpenIdUnencryptedAssociationSessionResponse; use openid\services\OpenIdServiceCatalog; -use utils\services\Registry; +use utils\services\ServiceLocator; use utils\services\UtilsServiceCatalog; use Zend\Crypt\Exception\InvalidArgumentException; use Zend\Crypt\Exception\RuntimeException; @@ -27,9 +27,9 @@ class SessionAssociationUnencryptedStrategy implements ISessionAssociationStrate public function __construct(OpenIdAssociationSessionRequest $request) { $this->current_request = $request; - $this->association_service = Registry::getInstance()->get(OpenIdServiceCatalog::AssociationService); - $this->server_configuration_service = Registry::getInstance()->get(OpenIdServiceCatalog:: ServerConfigurationService); - $this->log_service = Registry::getInstance()->get(UtilsServiceCatalog:: LogService); + $this->association_service = ServiceLocator::getInstance()->getService(OpenIdServiceCatalog::AssociationService); + $this->server_configuration_service = ServiceLocator::getInstance()->getService(OpenIdServiceCatalog:: ServerConfigurationService); + $this->log_service = ServiceLocator::getInstance()->getService(UtilsServiceCatalog:: LogService); } /** diff --git a/app/libs/openid/model/IOpenIdUser.php b/app/libs/openid/model/IOpenIdUser.php index 2cf9b329..cff70527 100644 --- a/app/libs/openid/model/IOpenIdUser.php +++ b/app/libs/openid/model/IOpenIdUser.php @@ -25,6 +25,10 @@ interface IOpenIdUser { public function getNickName(); public function getGender(); public function getCountry(); + public function getStreetAddress(); + public function getRegion(); + public function getLocality(); + public function getPostalCode(); public function getLanguage(); public function getTimeZone(); public function getDateOfBirth(); diff --git a/app/libs/openid/model/OpenIdNonce.php b/app/libs/openid/model/OpenIdNonce.php index e5bf50da..494f9a09 100644 --- a/app/libs/openid/model/OpenIdNonce.php +++ b/app/libs/openid/model/OpenIdNonce.php @@ -4,7 +4,8 @@ namespace openid\model; use openid\exceptions\InvalidNonce; use openid\helpers\OpenIdErrorMessages; -use utils\services\Registry; +use utils\services\ServiceLocator; +use utils\services\UtilsServiceCatalog; class OpenIdNonce { @@ -72,8 +73,8 @@ class OpenIdNonce */ public function isValid() { - $server_configuration_service = Registry::getInstance()->get("openid\\services\\IServerConfigurationService"); - $allowed_skew = $server_configuration_service->getConfigValue("Nonce.Lifetime"); + $server_configuration_service = ServiceLocator::getInstance()->getService(UtilsServiceCatalog::ServerConfigurationService); + $allowed_skew = $server_configuration_service->getConfigValue("Nonce.Lifetime"); $now = time(); // Time after which we should not use the nonce $past = $now - $allowed_skew; diff --git a/app/libs/openid/requests/OpenIdAuthenticationRequest.php b/app/libs/openid/requests/OpenIdAuthenticationRequest.php index e3374bda..dba68f95 100644 --- a/app/libs/openid/requests/OpenIdAuthenticationRequest.php +++ b/app/libs/openid/requests/OpenIdAuthenticationRequest.php @@ -7,7 +7,7 @@ use openid\OpenIdMessage; use openid\OpenIdProtocol; use openid\services\OpenIdServiceCatalog; -use utils\services\Registry; +use utils\services\ServiceLocator; use Exception; @@ -113,7 +113,7 @@ class OpenIdAuthenticationRequest extends OpenIdRequest * other information in its payload, using extensions. */ - $server_configuration_service = Registry::getInstance()->get(OpenIdServiceCatalog::ServerConfigurationService); + $server_configuration_service = ServiceLocator::getInstance()->getService(OpenIdServiceCatalog::ServerConfigurationService); if (is_null($claimed_id) && is_null($identity)) return false; //http://specs.openid.net/auth/2.0/identifier_select diff --git a/app/libs/openid/requests/OpenIdCheckAuthenticationRequest.php b/app/libs/openid/requests/OpenIdCheckAuthenticationRequest.php index 1107a92b..6792eec3 100644 --- a/app/libs/openid/requests/OpenIdCheckAuthenticationRequest.php +++ b/app/libs/openid/requests/OpenIdCheckAuthenticationRequest.php @@ -5,7 +5,8 @@ namespace openid\requests; use openid\helpers\OpenIdUriHelper; use openid\OpenIdMessage; use openid\OpenIdProtocol; -use utils\services\Registry; +use openid\services\OpenIdServiceCatalog; +use utils\services\ServiceLocator; class OpenIdCheckAuthenticationRequest extends OpenIdAuthenticationRequest { @@ -34,7 +35,8 @@ class OpenIdCheckAuthenticationRequest extends OpenIdAuthenticationRequest $claimed_returnTo = $this->getReturnTo(); $signed = $this->getSigned(); - $server_configuration_service = Registry::getInstance()->get("openid\\services\\IServerConfigurationService"); + $server_configuration_service = ServiceLocator::getInstance()->getService(OpenIdServiceCatalog::ServerConfigurationService); + if ( !is_null($mode) && !empty($mode) && $mode == OpenIdProtocol::CheckAuthenticationMode && !is_null($claimed_returnTo) && !empty($claimed_returnTo) && OpenIdUriHelper::checkReturnTo($claimed_returnTo) diff --git a/app/libs/openid/requests/OpenIdRequest.php b/app/libs/openid/requests/OpenIdRequest.php index b8d4d025..c7d8dad7 100644 --- a/app/libs/openid/requests/OpenIdRequest.php +++ b/app/libs/openid/requests/OpenIdRequest.php @@ -3,7 +3,7 @@ namespace openid\requests; use openid\OpenIdMessage; -use utils\services\Registry; +use utils\services\ServiceLocator; use utils\services\UtilsServiceCatalog; abstract class OpenIdRequest @@ -15,7 +15,7 @@ abstract class OpenIdRequest public function __construct(OpenIdMessage $message) { $this->message = $message; - $this->log_service = Registry::getInstance()->get(UtilsServiceCatalog::LogService); + $this->log_service = ServiceLocator::getInstance()->getService(UtilsServiceCatalog::LogService); } public function getMessage() diff --git a/app/libs/openid/services/IUserService.php b/app/libs/openid/services/IUserService.php index 3c4298d5..da43b997 100644 --- a/app/libs/openid/services/IUserService.php +++ b/app/libs/openid/services/IUserService.php @@ -9,6 +9,7 @@ namespace openid\services; interface IUserService { + public function get($id); /** * @param $id * @param $proposed_username diff --git a/app/libs/openid/strategies/OpenIdResponseStrategyFactoryMethod.php b/app/libs/openid/strategies/OpenIdResponseStrategyFactoryMethod.php index abb4ea9e..dbfd20fd 100644 --- a/app/libs/openid/strategies/OpenIdResponseStrategyFactoryMethod.php +++ b/app/libs/openid/strategies/OpenIdResponseStrategyFactoryMethod.php @@ -6,7 +6,7 @@ use openid\responses\OpenIdDirectResponse; use openid\responses\OpenIdIndirectResponse; use openid\responses\OpenIdResponse; use utils\IHttpResponseStrategy; -use utils\services\Registry; +use utils\services\ServiceLocator; class OpenIdResponseStrategyFactoryMethod { @@ -21,12 +21,12 @@ class OpenIdResponseStrategyFactoryMethod switch ($type) { case OpenIdIndirectResponse::OpenIdIndirectResponse: { - return Registry::getInstance()->get(OpenIdIndirectResponse::OpenIdIndirectResponse); + return ServiceLocator::getInstance()->getService(OpenIdIndirectResponse::OpenIdIndirectResponse); } break; case OpenIdDirectResponse::OpenIdDirectResponse: { - return Registry::getInstance()->get(OpenIdDirectResponse::OpenIdDirectResponse); + return ServiceLocator::getInstance()->getService(OpenIdDirectResponse::OpenIdDirectResponse); } break; default: diff --git a/app/libs/utils/IPHelper.php b/app/libs/utils/IPHelper.php new file mode 100644 index 00000000..c2b4a379 --- /dev/null +++ b/app/libs/utils/IPHelper.php @@ -0,0 +1,22 @@ +registry[$key])) { - $this->registry[$key] = $value; - } - } - - public function get($key) - { - if (!isset($this->registry[$key])) { - throw new \Exception("There is no entry for key " . $key); - } - - return $this->registry[$key]; - } - - private function __clone() - { - } -} \ No newline at end of file diff --git a/app/libs/utils/services/ServiceLocator.php b/app/libs/utils/services/ServiceLocator.php new file mode 100644 index 00000000..76b0b3a2 --- /dev/null +++ b/app/libs/utils/services/ServiceLocator.php @@ -0,0 +1,32 @@ +name= $name; } + + /** + * @return \oauth2\models\booll + */ + public function supportCORS() + { + return $this->allow_cors; + } } \ No newline at end of file diff --git a/app/models/oauth2/Client.php b/app/models/oauth2/Client.php index a1bc8e6b..d643c0b1 100644 --- a/app/models/oauth2/Client.php +++ b/app/models/oauth2/Client.php @@ -37,6 +37,12 @@ class Client extends BaseModelEloquent implements IClient { return $this->hasMany('ClientAuthorizedUri','client_id'); } + public function allowed_origins() + { + return $this->hasMany('ClientAllowedOrigin','client_id'); + } + + public function getClientId() { return $this->client_id; @@ -52,11 +58,6 @@ class Client extends BaseModelEloquent implements IClient { return $this->client_type; } - public function getClientAuthorizedRealms() - { - // TODO: Implement getClientAuthorizedRealms() method. - } - public function getClientScopes() { $scopes = $this->scopes() @@ -98,14 +99,10 @@ class Client extends BaseModelEloquent implements IClient { return $res; } - public function isRealmAllowed($realm) - { - return false; - } public function isUriAllowed($uri) { - if(! filter_var($uri, FILTER_VALIDATE_URL)) return false; + if(!filter_var($uri, FILTER_VALIDATE_URL)) return false; $parts = @parse_url($uri); if ($parts === false) { return false; @@ -113,12 +110,15 @@ class Client extends BaseModelEloquent implements IClient { if($parts['scheme']!=='https') return false; $client_authorized_uri = ClientAuthorizedUri::where('client_id', '=', $this->id)->where('uri','=',$uri)->first(); - if(is_null($client_authorized_uri)){ + if(!is_null($client_authorized_uri)) return true; + + if(isset($parts['path'])){ $aux_uri = $parts['scheme'].'://'.strtolower($parts['host']).strtolower($parts['path']); $client_authorized_uri = ClientAuthorizedUri::where('client_id', '=', $this->id)->where('uri','=',$aux_uri)->first(); return !is_null($client_authorized_uri); } - return true; + return false; + } public function getApplicationName() @@ -182,6 +182,10 @@ class Client extends BaseModelEloquent implements IClient { return $this->application_type; } + /** + * @return string + * @throws Exception + */ public function getFriendlyApplicationType(){ switch($this->application_type){ case IClient::ApplicationType_JS_Client: @@ -196,4 +200,39 @@ class Client extends BaseModelEloquent implements IClient { } throw new Exception('Invalid Application Type'); } + + public function getClientAllowedOrigins() + { + return $this->allowed_origins()->get(); + } + + /** + * the origin is the triple {protocol, host, port} + * @param $origin + * @return bool + */ + public function isOriginAllowed($origin) + { + if(!filter_var($origin, FILTER_VALIDATE_URL)) return false; + $parts = @parse_url($origin); + if ($parts === false) { + return false; + } + if($parts['scheme']!=='https') + return false; + $origin_without_port = sprinf("%sː//%s",$parts['scheme'],$parts['host']); + $client_allowed_origin = $this->allowed_origins()->where('allowed_origin','=',$origin_without_port)->first(); + if(!is_null($client_allowed_origin)) return true; + if(isset($parts['port'])){ + $origin_with_port = sprinf("%sː//%s:%s",$parts['scheme'],$parts['host'],$parts['port']); + $client_authorized_uri = $this->allowed_origins()->where('allowed_origin','=',$origin_with_port)->first();; + return !is_null($client_authorized_uri); + } + return false; + } + + public function getWebsite() + { + return $this->website; + } } diff --git a/app/models/oauth2/ClientAllowedOrigin.php b/app/models/oauth2/ClientAllowedOrigin.php new file mode 100644 index 00000000..0252278f --- /dev/null +++ b/app/models/oauth2/ClientAllowedOrigin.php @@ -0,0 +1,12 @@ +belongsTo('Client'); + } +} \ No newline at end of file diff --git a/app/models/oauth2/ClientAuthorizedUri.php b/app/models/oauth2/ClientAuthorizedUri.php index 5c626c1d..942d873d 100644 --- a/app/models/oauth2/ClientAuthorizedUri.php +++ b/app/models/oauth2/ClientAuthorizedUri.php @@ -1,5 +1,13 @@ belongsTo('Client'); + } } \ No newline at end of file diff --git a/app/routes.php b/app/routes.php index 7e8ed3c1..2b43ed48 100644 --- a/app/routes.php +++ b/app/routes.php @@ -88,9 +88,7 @@ Route::group(array('prefix' => 'admin','before' => 'ssl|auth'), function(){ }); }); - //Admin Backend API - Route::group(array('prefix' => 'admin/api/v1', 'before' => 'ssl|auth'), function() { @@ -112,14 +110,17 @@ Route::group(array('prefix' => 'admin/api/v1', 'before' => 'ssl|auth'), function Route::get('/',array('before' => 'is.current.user', 'uses' => 'ClientApiController@getByPage')); Route::delete('/{id}',array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@delete')); - Route::group(array('prefix' => 'uris','before' => 'user.owns.client.policy'), function(){ - Route::get('/{id}',"ClientApiController@getRegisteredUris"); - Route::post('/{id}',"ClientApiController@addAllowedRedirectUri"); - Route::delete('/{id}/{uri_id}',"ClientApiController@deleteClientAllowedUri"); - }); + //allowed redirect uris endpoints + Route::get('/{id}/uris',array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@getRegisteredUris')); + Route::post('/{id}/uris',array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@addAllowedRedirectUri')); + Route::delete('/{id}/uris/{uri_id}',array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@deleteClientAllowedUri')); + + //allowed origin endpoints endpoints + Route::get('/{id}/origins',array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@geAllowedOrigins')); + Route::post('/{id}/origins',array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@addAllowedOrigin')); + Route::delete('/{id}/origins/{origin_id}',array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@deleteClientAllowedOrigin')); Route::delete('/{id}/lock',array('before' => 'openstackid.server.admin.json', 'uses' => 'ClientApiController@unlock')); - Route::put('/{id}/secret',array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@regenerateClientSecret')); Route::put('/{id}/use-refresh-token',array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@setRefreshTokenClient')); Route::put('/{id}/rotate-refresh-token',array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@setRotateRefreshTokenPolicy')); @@ -173,13 +174,10 @@ Route::group(array('prefix' => 'admin/api/v1', 'before' => 'ssl|auth'), function }); }); - //OAuth2 Protected API - -Route::group(array('prefix' => 'api/v1', 'before' => 'ssl|oauth2.protected.endpoint'), function() +Route::group(array('prefix' => 'api/v1', 'before' => 'ssl|oauth2.cors.before|oauth2.protected.endpoint'), function() { - /* - Route::group(array('prefix' => ''), function(){ + Route::group(array('prefix' => 'users'), function(){ + Route::get('/me','OAuth2UserApiController@me'); }); - */ }); \ No newline at end of file diff --git a/app/services/IPHelper.php b/app/services/IPHelper.php deleted file mode 100644 index 1758be3b..00000000 --- a/app/services/IPHelper.php +++ /dev/null @@ -1,14 +0,0 @@ -app->singleton(UtilsServiceCatalog::CacheService, 'services\\RedisCacheService'); - $this->app['serverconfigurationservice'] = $this->app->share(function ($app) { - return new ServerConfigurationService($this->app->make(UtilsServiceCatalog::CacheService)); - }); + } - // Shortcut so developers don't need to add an Alias in app/config/app.php - $this->app->booting(function () { - $loader = AliasLoader::getInstance(); - $loader->alias('ServerConfigurationService', 'services\\Facades\\ServerConfigurationService'); - }); - - //register on boot bc we rely on Illuminate\Redis\ServiceProvider\RedisServiceProvider - $this->app->singleton(OpenIdServiceCatalog::MementoService, 'services\\MementoRequestService'); - $this->app->singleton(OpenIdServiceCatalog::AuthenticationStrategy, 'services\\AuthenticationStrategy'); - $this->app->singleton(OpenIdServiceCatalog::ServerExtensionsService, 'services\\ServerExtensionsService'); - $this->app->singleton(OpenIdServiceCatalog::AssociationService, 'services\\AssociationService'); - $this->app->singleton(OpenIdServiceCatalog::TrustedSitesService, 'services\\TrustedSitesService'); - $this->app->singleton(OpenIdServiceCatalog::ServerConfigurationService, 'services\\ServerConfigurationService'); - $this->app->singleton(OpenIdServiceCatalog::UserService, 'services\\UserService'); - $this->app->singleton(OpenIdServiceCatalog::NonceService, 'services\\NonceService'); - - $this->app->singleton(UtilsServiceCatalog::LogService, 'services\\LogService'); - $this->app->singleton(UtilsServiceCatalog::LockManagerService, 'services\\LockManagerService'); - $this->app->singleton(UtilsServiceCatalog::ServerConfigurationService, 'services\\ServerConfigurationService'); - $this->app->singleton(UtilsServiceCatalog::BannedIpService, 'services\\utils\\BannedIPService'); + public function register(){ $this->app->singleton('services\\IUserActionService', 'services\\UserActionService'); - - $this->app->singleton('oauth2\\IResourceServerContext', 'services\\oauth2\\ResourceServerContext'); - $this->app->singleton("services\\DelayCounterMeasure", 'services\\DelayCounterMeasure'); $this->app->singleton("services\\LockUserCounterMeasure", 'services\\LockUserCounterMeasure'); $this->app->singleton("services\\oauth2\\RevokeAuthorizationCodeRelatedTokens", 'services\\oauth2\\RevokeAuthorizationCodeRelatedTokens'); - $this->app->singleton("services\\BlacklistSecurityPolicy", 'services\\BlacklistSecurityPolicy'); $this->app->singleton("services\\LockUserSecurityPolicy", 'services\\LockUserSecurityPolicy'); - $this->app->singleton("services\\OAuth2LockClientCounterMeasure", 'services\\OAuth2LockClientCounterMeasure'); $this->app->singleton("services\\OAuth2SecurityPolicy", 'services\\OAuth2SecurityPolicy'); - $this->app->singleton("services\\oauth2\\AuthorizationCodeRedeemPolicy", 'services\\oauth2\\AuthorizationCodeRedeemPolicy'); - $this->app->singleton(UtilsServiceCatalog::CheckPointService, - function(){ - //set security policies - $delay_counter_measure = $this->app->make("services\\DelayCounterMeasure"); + function(){ + //set security policies + $delay_counter_measure = $this->app->make("services\\DelayCounterMeasure"); - $blacklist_security_policy = $this->app->make("services\\BlacklistSecurityPolicy"); - $blacklist_security_policy->setCounterMeasure($delay_counter_measure); + $blacklist_security_policy = $this->app->make("services\\BlacklistSecurityPolicy"); + $blacklist_security_policy->setCounterMeasure($delay_counter_measure); - $revoke_tokens_counter_measure = $this->app->make("services\\oauth2\\RevokeAuthorizationCodeRelatedTokens"); + $revoke_tokens_counter_measure = $this->app->make("services\\oauth2\\RevokeAuthorizationCodeRelatedTokens"); - $authorization_code_redeem_Policy = $this->app->make("services\\oauth2\\AuthorizationCodeRedeemPolicy"); - $authorization_code_redeem_Policy->setCounterMeasure($revoke_tokens_counter_measure); + $authorization_code_redeem_Policy = $this->app->make("services\\oauth2\\AuthorizationCodeRedeemPolicy"); + $authorization_code_redeem_Policy->setCounterMeasure($revoke_tokens_counter_measure); - $lock_user_counter_measure = $this->app->make("services\\LockUserCounterMeasure"); + $lock_user_counter_measure = $this->app->make("services\\LockUserCounterMeasure"); - $lock_user_security_policy = $this->app->make("services\\LockUserSecurityPolicy"); - $lock_user_security_policy->setCounterMeasure($lock_user_counter_measure); + $lock_user_security_policy = $this->app->make("services\\LockUserSecurityPolicy"); + $lock_user_security_policy->setCounterMeasure($lock_user_counter_measure); - $oauth2_lock_client_counter_measure = $this->app->make("services\\OAuth2LockClientCounterMeasure"); - $oauth2_security_policy = $this->app->make("services\\OAuth2SecurityPolicy"); - $oauth2_security_policy->setCounterMeasure($oauth2_lock_client_counter_measure); + $oauth2_lock_client_counter_measure = $this->app->make("services\\OAuth2LockClientCounterMeasure"); + $oauth2_security_policy = $this->app->make("services\\OAuth2SecurityPolicy"); + $oauth2_security_policy->setCounterMeasure($oauth2_lock_client_counter_measure); - $checkpoint_service = new CheckPointService($blacklist_security_policy); - $checkpoint_service->addPolicy($lock_user_security_policy); - $checkpoint_service->addPolicy($authorization_code_redeem_Policy); - $checkpoint_service->addPolicy($oauth2_security_policy); - return $checkpoint_service; - }); + $checkpoint_service = new CheckPointService($blacklist_security_policy); + $checkpoint_service->addPolicy($lock_user_security_policy); + $checkpoint_service->addPolicy($authorization_code_redeem_Policy); + $checkpoint_service->addPolicy($oauth2_security_policy); + return $checkpoint_service; + }); - Registry::getInstance()->set(UtilsServiceCatalog::CheckPointService, $this->app->make(UtilsServiceCatalog::CheckPointService)); - Registry::getInstance()->set(OpenIdServiceCatalog::MementoService, $this->app->make(OpenIdServiceCatalog::MementoService)); - Registry::getInstance()->set(OpenIdServiceCatalog::AuthenticationStrategy, $this->app->make(OpenIdServiceCatalog::AuthenticationStrategy)); - Registry::getInstance()->set(OpenIdServiceCatalog::ServerExtensionsService, $this->app->make(OpenIdServiceCatalog::ServerExtensionsService)); - Registry::getInstance()->set(OpenIdServiceCatalog::AssociationService, $this->app->make(OpenIdServiceCatalog::AssociationService)); - Registry::getInstance()->set(OpenIdServiceCatalog::TrustedSitesService, $this->app->make(OpenIdServiceCatalog::TrustedSitesService)); - Registry::getInstance()->set(OpenIdServiceCatalog::ServerConfigurationService, $this->app->make(OpenIdServiceCatalog::ServerConfigurationService)); - Registry::getInstance()->set(OpenIdServiceCatalog::UserService, $this->app->make(OpenIdServiceCatalog::UserService)); - Registry::getInstance()->set(OpenIdServiceCatalog::NonceService, $this->app->make(OpenIdServiceCatalog::NonceService)); - - Registry::getInstance()->set(UtilsServiceCatalog::LogService, $this->app->make(UtilsServiceCatalog::LogService)); - Registry::getInstance()->set(UtilsServiceCatalog::CheckPointService, $this->app->make(UtilsServiceCatalog::CheckPointService)); - Registry::getInstance()->set(UtilsServiceCatalog::ServerConfigurationService, $this->app->make(UtilsServiceCatalog::ServerConfigurationService)); - Registry::getInstance()->set(UtilsServiceCatalog::CacheService, $this->app->make(UtilsServiceCatalog::CacheService)); - - $this->app->singleton(OAuth2ServiceCatalog::MementoService, 'services\\oauth2\\MementoOAuth2AuthenticationRequestService'); - $this->app->singleton(OAuth2ServiceCatalog::ClientService, 'services\\oauth2\\ClientService'); - $this->app->singleton(OAuth2ServiceCatalog::TokenService, 'services\\oauth2\\TokenService'); - $this->app->singleton(OAuth2ServiceCatalog::ScopeService, 'services\\oauth2\\ApiScopeService'); - $this->app->singleton(OAuth2ServiceCatalog::ResourceServerService, 'services\\oauth2\\ResourceServerService'); - $this->app->singleton(OAuth2ServiceCatalog::ApiService, 'services\\oauth2\\ApiService'); - $this->app->singleton(OAuth2ServiceCatalog::ApiEndpointService, 'services\\oauth2\\ApiEndpointService'); - $this->app->singleton(OAuth2ServiceCatalog::UserConsentService, 'services\\oauth2\\UserConsentService'); - - Registry::getInstance()->set(OAuth2ServiceCatalog::MementoService, $this->app->make(OAuth2ServiceCatalog::MementoService)); - Registry::getInstance()->set(OAuth2ServiceCatalog::TokenService, $this->app->make(OAuth2ServiceCatalog::TokenService)); - Registry::getInstance()->set(OAuth2ServiceCatalog::ScopeService, $this->app->make(OAuth2ServiceCatalog::ScopeService)); - Registry::getInstance()->set(OAuth2ServiceCatalog::ClientService, $this->app->make(OAuth2ServiceCatalog::ClientService)); - Registry::getInstance()->set(OAuth2ServiceCatalog::ResourceServerService, $this->app->make(OAuth2ServiceCatalog::ResourceServerService)); - Registry::getInstance()->set(OAuth2ServiceCatalog::ApiService, $this->app->make(OAuth2ServiceCatalog::ApiService)); - Registry::getInstance()->set(OAuth2ServiceCatalog::ApiEndpointService, $this->app->make(OAuth2ServiceCatalog::ApiEndpointService)); } - public function register() + public function provides() { - - - + return array('application.services'); } - } \ No newline at end of file diff --git a/app/services/Facades/ServerConfigurationService.php b/app/services/facades/ServerConfigurationService.php similarity index 87% rename from app/services/Facades/ServerConfigurationService.php rename to app/services/facades/ServerConfigurationService.php index 08ade3cb..ec3a648a 100644 --- a/app/services/Facades/ServerConfigurationService.php +++ b/app/services/facades/ServerConfigurationService.php @@ -1,6 +1,6 @@ first(); + } + + /** + * @param $uri + * @param $client_id + * @return bool|int + */ + public function create($uri, $client_id) + { + $origin = new ClientAllowedOrigin(); + $origin->allowed_origin = $uri; + $client = Client::find($client_id); + if(!is_null($client)){ + $client->allowed_origins()->save($origin); + $origin->Save(); + return $origin->id; + } + return false; + } + + /** + * @param $id + * @return bool + */ + public function delete($id) + { + $origin = $this->get($id); + if(!is_null($origin)){ + return $origin->delete(); + } + return false; + } + + /** + * @param $uri + * @return bool + */ + public function deleteByUri($uri) + { + $origin = $this->getByUri($uri); + if(!is_null($origin)){ + return $origin->delete(); + } + return false; + } +} \ No newline at end of file diff --git a/app/services/oauth2/ApiEndpointService.php b/app/services/oauth2/ApiEndpointService.php index 812d3008..c49c921c 100644 --- a/app/services/oauth2/ApiEndpointService.php +++ b/app/services/oauth2/ApiEndpointService.php @@ -26,6 +26,16 @@ class ApiEndpointService implements IApiEndpointService { return ApiEndpoint::where('route','=',$url)->where('http_method','=',$http_method)->first(); } + /** + * @param $url + * @return IApiEndpoint + */ + public function getApiEndpointByUrl($url) + { + return ApiEndpoint::where('route','=',$url)->first(); + } + + /** * @param $id * @return IApiEndpoint @@ -51,16 +61,17 @@ class ApiEndpointService implements IApiEndpointService { * @param string $name * @param string $description * @param boolean $active + * @param boolean $allow_cors * @param string $route * @param string $http_method * @param integer $api_id * @return IApiEndpoint */ - public function add($name, $description, $active, $route, $http_method, $api_id) + public function add($name, $description, $active,$allow_cors, $route, $http_method, $api_id) { $instance = null; - DB::transaction(function () use ($name, $description, $active, $route, $http_method, $api_id, &$instance) { + DB::transaction(function () use ($name, $description, $active,$allow_cors, $route, $http_method, $api_id, &$instance) { //check that does not exists an endpoint with same http method and same route if(ApiEndpoint::where('http_method','=',$http_method)->where('route','=',$route)->count()>0) @@ -74,6 +85,7 @@ class ApiEndpointService implements IApiEndpointService { 'route' => $route, 'http_method' => $http_method, 'api_id' => $api_id, + 'allow_cors' => $allow_cors ) ); $instance->Save(); @@ -95,7 +107,7 @@ class ApiEndpointService implements IApiEndpointService { if(is_null($endpoint)) throw new InvalidApiEndpoint(sprintf('api endpoint id %s does not exists!',$id)); - $allowed_update_params = array('name','description','active','route','http_method'); + $allowed_update_params = array('name','description','active','route','http_method','allow_cors'); foreach($allowed_update_params as $param){ if(array_key_exists($param,$params)){ $endpoint->{$param} = $params[$param]; diff --git a/app/services/oauth2/CORS/CORSMiddleware.php b/app/services/oauth2/CORS/CORSMiddleware.php new file mode 100644 index 00000000..1a5c169e --- /dev/null +++ b/app/services/oauth2/CORS/CORSMiddleware.php @@ -0,0 +1,203 @@ +endpoint_service = $endpoint_service; + $this->cache_service = $cache_service; + $this->origin_service = $origin_service; + $this->allowed_headers = Config::get('cors.AllowedHeaders',self::DefaultAllowedHeaders); + $this->allowed_methods = Config::get('cors.AllowedMethods',self::DefaultAllowedMethods); + } + + private function makePreflightResponse(Request $request, IApiEndpoint $endpoint){ + + $response = new Response(); + + $allow_credentials = Config::get('cors.AllowCredentials', ''); + if(!empty($allow_credentials)){ + $response->headers->set('Access-Control-Allow-Credentials',$allow_credentials ); + } + + if(Config::get('cors.UsePreflightCaching', false)){ + $response->headers->set('Access-Control-Max-Age', Config::get('cors.MaxAge', 32000)); + } + + $response->headers->set('Access-Control-Allow-Headers', $this->allowed_headers); + + + if (!$this->checkOrigin($request)) { + $response->headers->set('Access-Control-Allow-Origin', 'null'); + return $response; + } + $response->headers->set('Access-Control-Allow-Origin', $request->headers->get('Origin')); + + // check request method + if ($request->headers->get('Access-Control-Request-Method') != $endpoint->getHttpMethod()) { + $response->setStatusCode(405); + return $response; + } + + $response->headers->set('Access-Control-Allow-Methods', $this->allowed_methods); + + // check request headers + $allow_headers = explode(', ',$this->allowed_headers); + + $headers = $request->headers->get('Access-Control-Request-Headers'); + if ($headers) { + $headers = trim(strtolower($headers)); + foreach (preg_split('{, *}', $headers) as $header) { + if (in_array($header, self::$simple_headers, true)) { + continue; + } + if (!in_array($header, $allow_headers, true)) { + $response->setStatusCode(400); + $response->setContent('Unauthorized header '.$header); + break; + } + } + } + + $response->setStatusCode(204); + return $response; + } + + private function checkOrigin(Request $request) + { + // check origin + $origin = $request->headers->get('Origin'); + if($this->cache_service->getSingleValue($origin)) return true; + if($origin = $this->origin_service->getByUri($origin)){ + $this->cache_service->addSingleValue($origin,$origin); + return true; + } + Log::warning(sprintf('CORS: origin %s not allowed!',$origin)); + return false; + } + + public function verifyRequest($request){ + try{ + // skip if not a CORS request + if (!$request->headers->has('Origin')) { + return; + } + + $method = $request->getMethod(); + $preflight = false; + + //preflight checks + if ($method === 'OPTIONS') { + $request_method = $request->headers->get('Access-Control-Request-Method'); + if(is_null($request_method)){ + Log::warning('CORS: not a valid preflight request!'); + return; + } + // sets the original method on request in order to be able to find the + // correct route + $request->setMethod($request_method); + $preflight = true; + } + + //gets routes from container and try to find the route + $router = App::make('router'); + $routes = $router->getRoutes(); + $route = $routes->match($request); + + $url = $route->getPath(); + + if(strpos($url, '/') != 0){ + $url = '/'.$url; + } + + $endpoint = $this->endpoint_service->getApiEndpointByUrl($url); + //check if api endpoint exists or not, if active and if supports cors + if(is_null($endpoint) || !$endpoint->isActive() || !$endpoint->supportCORS()){ + + if(is_null($endpoint)){ + Log::warning(sprintf("does not exists an endpoint for url %s.",$url)); + } + else if(!$endpoint->isActive()){ + Log::warning(sprintf("endpoint %s is not active.",$url)); + } + else if(!$endpoint->supportCORS()){ + Log::warning(sprintf("endpoint %s does not support CORS.",$url)); + } + + return; + } + + // perform preflight checks + if ($preflight) { + return $this->makePreflightResponse($request,$endpoint); + } + + if (!$this->checkOrigin($request)) { + return new Response('', 403, array('Access-Control-Allow-Origin' => 'null')); + } + + $this->modify_response = true; + + // Save response headers + $this->headers['Access-Control-Allow-Origin'] = $request->headers->get('Origin'); + $this->headers['Access-Control-Allow-Credentials'] = 'true'; + } + catch(Exception $ex){ + Log::error($ex); + } + } + + public function modifyResponse($request,$response) + { + if(!$this->modify_response){ + return $response; + } + // add CORS response headers + Log::info('CORS: Adding CORS HEADERS.'); + $response->headers->add($this->headers); + return $response; + } + +} \ No newline at end of file diff --git a/app/services/oauth2/CORS/CORSProvider.php b/app/services/oauth2/CORS/CORSProvider.php new file mode 100644 index 00000000..56ae1e2c --- /dev/null +++ b/app/services/oauth2/CORS/CORSProvider.php @@ -0,0 +1,28 @@ +app->singleton('CORSMiddleware', 'services\oauth2\CORS\CORSMiddleware'); + } + + public function boot(){ + + } + + public function provides() + { + return array('oauth2.cors'); + } +} \ No newline at end of file diff --git a/app/services/oauth2/CORS/cors_server_flowchart.png b/app/services/oauth2/CORS/cors_server_flowchart.png new file mode 100644 index 0000000000000000000000000000000000000000..cffd59741659b490a32f8651adfc1deb1b403c1b GIT binary patch literal 99960 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

4$y&@xqh>-skAyeW`C3gCLhw`95cH}r5ZI*1N z6SQ9S{jU&k)mIpo`FmJ^N_VeQ{?(~d>0KY(qdWfXs=&RTg{~?T|8`#>_C8;Qz1Uxs zek;nOZsTZ!c6Kli=>N@W69Z@;+rW>g=4NsSXiaEK78w<7b2Jamn9|E@d}^4xl=pNM6%72{~Dmv%<&S) zW2GiNR9MZLgM$-PrhUMq;s)^|#VEIVnwQ|!tuS#i7Y}6E7Q_Qn!$aZkFfqr0uezWBRA~12=TUr87DUyA6fgX}rS@JHiKW~v&<3-RT7T1_ z@Y2ZnR9#J{_n7;Eizp_JD`0K6$dDdO`Pl*@7gZ3Ik=)$RjOWsc7nrG?twho#+Ax(FUPGcvmzK6{ zc@p!}Rn`Dx-A&X#P>BwkAk%68FU-muA7*s^|@>cbzp3kQYXc23irD%*OAV zEjcdNVelEl|GRMeM#}1E@A- z&F>EfmCyZN3Kz^QG9Z!TYnV57i75YYS1`CT9B^e6in6XKdF2Oieg@paF5crdUQwS) z?R5!Fjz#}6d=jCQd3kOOQ3cnb4Mx)Eh?XmOu_U^8-{^zqbI4-gm>}vqa>7AesI@b<$eQRIN z_6TGMTi=mD=2AN|hV$yzu75wBB!v9@Lb8{fM^{Y2K4Q-3Bl6iCf8(g;>}h!Gn0wv1 zhnnB=%vTsCyDzuGIFFb~EvO;NAXaQFUb39?1c;({RqnL5ww5DAJV7Krj3to_P2>X@ ztv3WQ-~Hf}%DhuLfaZDnbxmX++~g>x6EDqq`62@cG|9Mkbma#H8Fi&^XogYce@WUM zf&t@zVr_I{0b5PRWnzDpQ3&R@J!%-bNi^#HG`V`Db-6gS!PaT-&9O0}1+pfxFM{+< z`z4h?nwy)yyF^7rp$EX*xVQO{f%qq~G<)AHNFWxw6a`b24%w zP`-BIdek8K9sR_$px9V>Bs#q$MV7YU$Q7-!T#m4}CbIy1-1#Vb%A2vbadWKz_s0nf zH+Sxwyv`U=?jnZ5p#@swN|LM5*PI?-s|))PU70YV$4b|J3(d#xqh1kqG+A9_LZ3br z<7R?OM)7|%?p~J5rl?2to)W-(ei9S*pn-PH&{K!ZApZn^lBD(w;h$N6q6$jgU8Uck z|9xnE$-a)i_GA9TgShcZFhvQ=p0=fHg5r#BTNGbR&3>VIZ}qoj<I`9oX8{kFit z6VD0#Mox)~)FIEa;!}<)k%^xsJ5|N%P^dw*V$bmBLYmr$F?e${AfvLGwdATyR!wRU@D9J2JFlV=)DItAn;kS{Pn z0fEW6ISAXr`S~OwrX+z7*V7*}N(qv4{EKAYp0h*urI zNJ~1MWzCKqB_9Pr+Hf*CaAI2h)zM~<)f?sGnQ)yF#nE+WCvh1&b3?0Z8Obs7=X@I3 z`9prwC!2LXULw_fALm-n=DBwx%p<{l?zDHUb$`cP@Dr@zrrm2=sr{_1235FinfQSk z*}C3@nG**Ltl!cqy-VZog@&^Bd5=C)p5@^J~R9rkp>2!+dppgYQg;A9hpT(eVg++jAu0VwW zTU1O9WFiXw;kf}*xE$#HV#4A7?Ao%=b?&TMe^7k^O5I>Iyr+?4Jcj=E;o_c>@ey9v zxw{O`fo~HraB2GPV()GetRaJZGEK>s))|*?nYeE#CezDq)Lvg=MU}L5LJ<*9rN7_H zfDl`ibv9m`Qs39^vwdSr2%?nK+uf{IIGwlHg52t$BHXKXf}y7jEOF|fG+Ovgf=YhB zXBie72Q7zA^M@uU@pYsdbm!<6h9Lg1wwu)tG~wT41wNzbdzpKatoF#E(eSAjL9UWn zz2@bwrOz~#qgBcdzbjCM&V|9kR>EMcSke;%b4{}@OY!xSd!Vnce;*&afoQPTmzRM{ z&~=xd7R@_9f1~Lk64;!EG0+Ws7&eyv2REPrant&Ma_3`b_7wA*Fmc4ndTSP+I90O= z6$jMs%L=X6wBI(aF_D7lui z8PcU*swFn6czsnld-$wk^kW)bh@h&grNcgH5k!E}xG1+~t*JmoP&PKim8NlRE6LhK zCD2EYiU3ov3q1ESMCQx8_M&>d>$E!ROi_NKIk!p9M>1Sn?X zYx=q~1=4Yfsrz#r1d^U@P$kGME2sE~3G8>Y6G5eUq}p}`Dy$i72j6G3wd^`nUbK6momzWo_(qwz{;JkaH?tC&en;|5 zMU~8H^C5@dbE7lmgBVy=+(D%@`zzV4<&e1ffc#Bnj11CQ$wke1PZs4d9C{;$kKY#` zyJPs~H^vp@tO5BESpxG#l#|XY=0fsBq#gHZ7Z^}CNjle3lj3Q77Cq0=*+H6zw)~>; z($~0}-A9Fj!*VC|o~SUN{I5xF*E9iMW$5u?n&Nx({{Rzozm~rK3?Y9~n3%hmlW5qJ zVFnE^rU;+u=$9w{&Qho&_P&IizBg4N-&ydnxyOt4tu{P(=@_M@u{R%}j`=vDBxQ%* z{dPNFep@a9NCbF)=B;)a_yswn;o#-V&*_9Zu1hX2fAER9E{%QGt?vN@qNDo+--9+u^W_?O)B+I}RSgaqZP{=QUfh!)xVVAjOb~*{H(0HrQ{g3dO(u=>~1j za=*kkBX~FTT-B0wu<3ztg()fxC)}etTa5}hc?>>Q+&~0FHHNsv(Gg!l!;5G17_7)d zk@S}oqIzGuaxK1Kk$g(^LT%`|uMuX*hOmltRX#*B>&j75<#pf0 za_VZ98zjf*-`XnkfZFU}3SAo)JEyD4*x&gE){d@5O*H&`x|p$8SeZdJ=f`OfeZLg& zaKIs#$#j6rNWlj4L|~L-rTtt(;y+TE1|GXgMi}7?bnp*GuH{4g_~+bQ-mQUsx0p#; zOsYD^j^o=tfE%%sq;C2y`sbqRz%BwU9njf^9bhAWx0~mB4in7kfQA$_Sf9=G#Fvib zkN88ky`P*N2dWHw7oEN8sYac3#JM;cPoqwK{z3`FTsK)`;0O4{?l#{ac&`1ZZ}kMb zJm%$aTuD*EZXS&nmM8;x4_iTve}8WfWeGXj0olVYcrXQ*6{x|6W+PaK^TtoVJkNm2E2RJ#UN-4mzRzjf+l51;VzRXBD0P90K94%lKND_%inZe z<21M*PJEG(W&^g!NG7Mk*S<5$YlJQIT?w%PoA^5)2;6b%ov#FX=To^iK}hc1uD;~X zrE;J2^?sv|*-_vR`cu+U{?2!z7cp?~XPCOYOIYO^* zT_Sfq6F1rDs_3mKX}k5h+^2lKOWA_8ar_xD7@-9wEgOJK!n578|D(Xaq%r9qWo15Y zntjPd@r&r)t(zlwapX(L2H#uNi4&@OYgunS-k)wN0!5JMXeD~g6`dveKorhmefG+H ze>vP-SvAbo{@!8V=-EC?aqAuREh;WJfq_M^X=eyK{?xF$prY{_rOj-f;%ScBym_DJ zK>R{FlP$ z|ILwm%L?@8NMQ4i^Z(ABdrMgK=gt3QBl_d<1>XFjGWzG_;P0FNTVZ%%4{_imBY>v# zwSC}za?ArbUnbIfN+qZ3fP12_fNKob*;2*!Q z#On=+X%WiXVnmA@*0v0vY0*r&sH{y>+(#G58=LudI_^Ql9P%w##%Pkk^uvj_KnIu3 z{vr6N4_+Lg*BZk)1kP6gPOf5VvP~xeKXBaP%--7V&3vabNi>}tNi8irIMwSxw5can zda`iyyLqf?9?#Ke5pD})v4n3+8cpEA(R402eJ~P8SH8=efYUJ0YFIE51{{HuPHa^b za;5gs!DYg-ScgaZYl-3ZVd>>N9^~*q<5-^zIUIfA;W)n^iTJL1iO9l%vQ_!YokA%}(~V`P{p z5gq1LQ@J0qQ4r?evkQdaQZdD2zTC3NDBw=DL*=QiFV(*&;H~{gGv0gV zBSQW@Lxl;FKXOg~lN?Q*|8JNja2AhMVmD_7%KM`tfSVyO&S3$D%im_rF-r z*3Dd|_;PQ3S^fpy_3v+No|*1zPA{GAZ!QaJ8r{0cX8=|~yHw{qzIcS^J$!P;n+mK2 zY>@m?*KE{=;-6s$&o`y7Z%&lgwzsK0nuss2!DuaklmWF@vSZh{JP|65Nd%C|@Wps2 zWe2kn_~7GL>zvp*IYe<-)+(uv{;NFQ__*ijPpi7BI;c0Xdxk4mUY_|RzlH=STG^7J zeCvTW|LUy&JmIIW<~LlEd%g|)&z%-p9cJcw%Y_WL5rS-ODC2Am27WUv&3Iky*4yf$ zweVpEXer)(P5N%#>p(817ly1~a!GZbmofh?wiE80pmjLDoKR|m)9t(SLyvXe`lbPy zprks_9#xU*XzlPpd=FpoT<9jL+-57f%ciw<>#hdAbnNNQPYym1FO@9pFPIMQc?_fm z!|#{h`^iP`zvNjRywFwZ{bIYviJ7?LBN!3@{}CJSoj0Go;oN~Llz{W6*pl73#~Np6a@ztTz>Z|60bv{NAc(~a z#cid2VOa3xVv;Qh zocDdU*jvB!tw2_tW9=R1i>73?{2;6?xBk9-GvH7T-jw4`^s$RuXaD`xf3i@7brscZ zV)5jZS)WI(IJ)Kwh`2|HA`EpEMfvkXtpV$Bwl-c5%&4;UEcu(VQ-0Mnu_ir^CJ4jQ zPN*UMd?47^p|so+Znu3lUC0_L^@qq#~lizInsu)~^dVc1gjVE7x5^O;jJ@8!~n| z!)YlJRut{`VuRt~BA;LT9cBR8A9rw5-#Vl-!QJ(;dp?%K^%-LtS|%xc+ezxv|F*2$ zdEYKI4+kJ)V?nRi@9~BZ{XJ!-wcko@Ji}aY0a&V_w?=P`elu(XEH{9Unwgsxi3ge( zv~{r+1GMN+3!~kp+ul6-@K2A!csbT6NQp^x)&%WRi(!TY;~!FQ8?@HB#`VDSII;Mr z7Ya$vq!!)v-Sw8k)R?b^D^T@K?m8@W*WJgJ2ZEshIT9S)=s+WB^3^w{CAy_eF1c%E zG|=>Lg~)(H8ysA44j8Rz(XXUfxYKaV=fw`{p2{@>t>;Pl*;}Kse|r{niaB`V_Bzi& zIagFqxvgElldlxIWdR3wP7SoBC~Fu!gLNQg{<`1M@jI-R+mRDKxoW%lthbX{tThMd zoe-=4z2XIBI`c-Af7BOmUam)<#yDg3oRm`S%?WsZWyd=D6%S~GK?6`0kI+EJdAr<= z8QQ0GJ@PtpZ_CF_>vBfD+C(lZQR)NXlC{pYiE*ideV@v;2*(EK*F0*B1v0(S4&rsJ z1REng4C|KUz+b{IPdHf&1Je(zw29iEZRrycGsWA4TCxwz$r62fpBx59CNuf&Ua|V5 zI(lDj6-%}_lySqHZx(%Y)u>La&;|zRONBf%)JVW+DI)N0BT@<>?HJN%RB-R4-pj|# zz`xl<3lm2khDYa6Z;h=-L1WTkdXN!nW8<;sK9%=$3=q8FF9c}#eHbXmWuB%D^r(w|KQ?27 z3*9@uI2{mMYvs5jMd@9)R<<&H?)tj<#;elHs>bIl+t)!l^xwxD#LiLnWW6StbyODo zi^AL zev1s7O8-oi1v#p8NDT!uaUj0~dxV}u8uZr{Z<5R7=9yjbB?sSVudeEP1_B$5QytFr6{Rm);>BtFF?c zS++MX&}xleza1Ek;7d!}OIWd%IUvKviyZb9Vk6?j*I~$m9BhckFbT0l4Pd4)`0x)#ykx+bdVTBp`}SEf$&h*MUL>&V;QfwP%~>B=XlUCf0Po6^pZ-Q3Ey zKu#5(46%ly7iFA-)ttA5@R*hxt!Aq9O19>VZZ-Xc@;a@s6&fqml*yZ1j6UNk$QX)~ zXB@JV-+%t-r#Ywf!$7oQ4Pl|PL7eoW%}u70?yu>**l_E1Mpd&o7S_g-Pn7-c`4x=h zK!VnZ&Ii#0-jGh@kvC=sO@TK^gS6P?@H6{uZ+Osqx}4rRIME0QET95_&+|@JRB2Q& z+HThd9vl++j~XHzFM=Cz%>-Tz564U4JzbosZQmr5pmRfF`J`KN02?3XwDb3eP@LF;hrl17;xK^ zgkg%F`lwgidx(K}h5;Kf6Zi>oMWWS*o-tA)->}v#k*<%!KetEvtcNi7gq#l+6HsO3 zz{Ic$0O`INLi*COmqjf!Kd4uR zN#LP&6Xn-wryTh*Tf{I&>~Y{Rv#8vZsJQ}WWF`{(jAn^zRSChLfZ$8kPL0mjo>kuoqmhV<>g6DJ%Z@Jw+e2DRKalLN(d%uP;X#V(9}_;ujn zylN4_x}2??<$-iv1r=;)7xaivaieS3ynMyv6$-2c^_XSE+Ec?f-{9F?nf5Cj#kR?$ z-aNA;E7I#9x624QqnKDFhVZw4CX57V$;vSI^hai-eYSIHlP=CB%bK4~>iIlH7}y3m zeh7VT;ZMmO_$CG&z?9ZOT3t z)6sXX{>tOa9t7u8FDGWD*UBwm{`h!8OicRQQvt)Sq391$z;#K-u3gs>5z5V4FZ>H6 z+xIkWLGavf{r&bA#Eg!mh;ZVi{My|*i^Z>c6J8w_P1WMGGf%&@K*i>_I>Gs{T`iHQO~!10>}{JCQiuze z-${Ekw)DKfjGa4bkD5Dq>xMH$;G%Gf#&-9IDS0W{AZ!ZShvoNEMYN*$dOLCjaNzFA8hDRWQtXrjmQs}S)Z%1O`0cLNkA$> zBU3%xsIWzL8>31g2lAS)u!!F*H3)L0A7EDlx49H|1SjidY$uB}R2ukxd@60ohFlPO z92k8zo`|lgh`?{EkYf;A(@YV>R+3@dcPPrqF(1FoSvQ{oYhC#3zwo!^K6l(@Na$W_KEae?e}RdmaA;M zac{l}7_KlChs#6-pz?XeK_f(><-{GlJ1!WOzYO}W;^HW#xDIvtD^g!BzwB>jM8=Er zq3ZPiCaPKxq1C|=Fi?wGsQ4N!j+r81P2Z@u#mPpf$&d$K*0tQW{_q0$^5{f%k2UUQ zK^7h@WZHVIkNEmlN992L4_~>CuQilz?b8YyeYFGTbJ*U?Oo;q)%W}wdOkChW`WFRO z@CZNvGN0HOW#C5_eq{T(bxtP#}I)iqwivDh=!dtTScN$yj#Fx zlCP_R14~$8&i!oz%ACYRtH1gN{7ynyw{G1Gaj{u7)FC`xD_c?T#qLRM;cmz4@^gt; zaHAK80GHh)*LqozduZtb@$ry1RC4X5Z8o8V@(UNq&6iWLWyOmEH@M1P@RYwQ1!|a? z#Nb%Y;b@)^kLJpe&?`SeN5Jn;XGTYQL>CsAC7m7S(+k41@643N+}*}Lj)yTm?h=~y zCs)=u(>~MJ6V$WbsqrMVQBQqUKKE-un3E&J^r+E$8xti#LX3n&H$+75 z(R=hddKaQZ?|m5g&XDK5@4er>UpYU+KD(~H_gd>5v;S)??E=C!1=uWlpZ@cMJ2BMj z@o$l*-zR78nB1uIVU6qBrFcW23Kc;I9R~*>mzDK@uU*1k*v+_83yrHToxERDsOyl&{iNTHYP0%?f>e_FcDzHHAf?aucn^mV$Wip563iBV9UNpsEQ;6d zfd6;k`}4u*`E4_YDd407F&26 zH0l{}hZ%s+g5sdG)gu-mVXjRx7uL76wL0Yc$jKbvRyL^2L{JB;`uXJ*0-phoPsA<~ zg*#39M4DJHVyuq~jt+aY<@9#-#HRQOTlk*^YFw~9q@a$%A95=S9-sJg_Mo^I{v5c* zi98yj{+Rbu+|6)w+MfSY+q00hbL7slk67rgc!r5VhGqw?h8$XRtQ2?_wced5(#6GJ zoajcrT^SUFP9NoUdF3)6*of7pF2r`#xUCE=1d+)|^LF020$nFc93LCYy@id*ugx%r zyqCF3=F(WFQkT*%8cA`lmTJ!Pj;*A{T~Nil1$(0I2(d z_sB8)#j3L_QZq9t7+yQ4O#ie`h|99z@R#{cE{j4(Sy*W?S@9NZku~92&ed|w6%+S@ ziX|ZUF0`Py<>`BFUkFLm?&Q#x?OoEXt(K}2!vR+NN}oJ4J#B$mlcaoa5W$Synw7P6 z<>{iu*wuwc(=UuZ*pckMCVSrtUlTI*N4AITwLZVE*kf>G;r6Vc;}3y}mY$ylT@$^q zWtRDfNziM*0pc012#-)CvAVZb066D!RT^a;8%f>!-sB@`%qyiuGFqlD=-p!vKC!t2 z(pR3Y2MNWSmU9=TF6pry3d=60ofuX=Wox~WCl4e16nJRw2DxM##kH=%x*NepjZH6z zHImvYXR+Vy9|A5n!ye4x{=vt<`2%k*D(_M=J#@Et+!Qx?ScXV+>G(mpHayS2x?|Yv zuxU8gA|X~s{9C0e5Jw5b5;MO|Lh?ynvc>_oyBjxQ*#plhcYjeK7T=*)$hvzuQbU7S zk&^4Nk1q%*w~cXO{_4tw!gJ8Z1gfedH4{5JDui_8K;(x5V=?W-qu(8e4y+9~hjOSMF) zE@k+8ZNJa)@k9Ioun~+)*Vb6r))YkT#9dAiv7o3CJsxN>IAT}!KN$hSrQ(S5m>e_e z_*wU^Vk0~j7n0Fxtoh%@JjpA|OdPrNO|rR_rB_JX=^Rb3jeCo8>A7RC2-0?{t?Cg5^jgE3g zN#mPAd{fbl$)`e8k(?3b-k?_Nnkyo`*2xgYN_oF$N0y&f_N?7pD8du?tyPLpwc~GN zeDj{>@yzZV_47S%z9Zkfr_xadaJn7vc1nNz$wgQZ2;jqq0&H_`Ww&Th#cEXKv~QYz zIibMbZb}ce81rjDkrg*h;7?VeQPuutD!QB_(u;b`;pcC<+?OjCGcmUAc>`PE zqb?%z=@~uTRL^^Hp_m1BUl2Yy@<|M0lWOuKw<8Hn1BhniC1>WAtgjD0etv}MK8C!~ zQ)SFrn)xMc5W$=RX_LCDd#a&;{;Q+G4A>X}DsQ_Y&lX5f>|Q8fFM7Hy5jMLt#W11K zn8pw;!LKBPel>1M_`UV6z-H`QbE|4~=AIWMakpu>L&$fRxYWdyU@|F$DS9!yP=A{2 zmwjoodaQnFyk54`tcG>mq!YxWo1ynVKOaiJn%H?SWWNUt?&>zK$tlQQ@8xy#)KUt|5b~&5#nWN`mS!Wi~ojq}PZ_X5Q8}f5F*Wi(WHGP_=avZpZ zlhl2A$nEkx$sTsdJmN4w+MQCiQs=~_nD&*)znGC6pw)_DzV(5M+ElaFBdMj8db`G0 zmpR!c{%RQ|#WyeC$+*xmL=xsne&bG%FnUC%V^yK9b`l(YG3j#n?7=Y4nd@HPqa-?{R`#;I7nPON2nF)|cY5HFhFZW=*3d5dKh`Uk0mp3c?H?i!LJt2S@h6_d z)w#I`(ttO|@insH3)rZjb&q<$!4VNC*$@H}@P(=$Qc4J#ES%%gxaTLyda*B;S%mBrTtrFWxoym^edM zCpRlSW=6R+0A4W9BAq&noT#|7AG#e}6qzbKmdkP1Kvq%za~24|gYgu9_UjO=iB z;+MGVyw9~^?vIZ3Dd)U#%-@5Nj(c>mGFK0!pAcD8 z=vQN~#)Gx5#;>44E0QBaHy6{MVW^@UM=zFyX5sZeXff@LzV^@G8jBWK@T(q-9@&o8{4SZieqFufq8q{qZn z=be1-r)NvTBVQCY;{H(gIz{y0!{Jp)zW1j&nT(Pvzc*iLab%)jL8xZsB6hwQ+dt~f z&uiKGsx&OJFvO~lx-9^Jm@qa0`pl;X<3fl*SH$*0LrHa|9@4|ZLp01&eR>AIKi7`g z=5|+fuqB?Iw>|N+C`a^^c%JNV4xlQ-UhD_I+i|p^T?Iw&q629Fz_-dbCAais_4;;b2 zHOa%iKNZ(WO+qx>z@61;gy7)__-%$bM`Mk8iLUN2rz7SShhK!Qz?#bp?f^k=p7>!i zxJIrFM~IW|Qmnz~cDL2UZ9{MXl#hV?DhM5!d1x0F2)kZ=GAC0`CMX!~DZoU(#X3 z`%#hRkY&;1uh9?w;uuv8*DCNSj|l(mw^vL)rat`kd+9OevDFT*LK=;A5$mDX zQ57rcJ#rPuL$%nz`vDk?1}AMUH)b{+0^}OH=x-ak79W|$o1Vd1_ln+F>e$H*?=j3` zU)|2l^)>~<=$HadmQuyt?%}=)AQw7Y+o0J_ZeVlnrJOIwX!bg-3gk!tA72HxAkBcI zQ~_+D0JelZGGh?%y2Z1U)7KP{i|&m4P^F{mXX0tO1}vmhL~vr?o`%u@{7!S43Ird8 zC>`m>KxI81bhp7pwz*6I<#;Q~9zW;NYkND2Q`+t?hAXhi&fKE{<~`z~hhJN*x+KfE zm&rc$Qxi3FQ$e4-XW+nSbz^xexbG~87QOdZVtq&z!LRP11l|uaqQ7&`^L1Vpm#mG#hYwC+Z)Msdz-V8quk?#ux0KW zJcNrL#WRvP#{tYqfPNRk(NxfE$mqkiW03j!hiut8_M^X_q1w<@+APN`;!R1w;>oDjJ2HMdKEJ(WzF*8HvHV zI~oTQIT?qqFw%os(8$;Nz`<^VKtCAt4PvD;@q`hzk-A%$dtj>9fdS6Ys0BO9K#Qi> zOKuk`y~=;Tz!KE$HuQPv+nZy#H zS(yjI0>CIf4`Dw9jJ3s$qX0ZWKRDs5@%T7c!%KY_-L24kMKL<`qHj$a#5gM7x^{JO zr&#u^3x0UROv1dQpm`$TbtKk4E7CDF5*Uas?u=sE5RA$0Z5+r5n+!O;*)`R>ZC=c% zeey&E!~?}4()obSvU+CDsl71ZI{D3bWknkWJPm(ajVJ>o@yGn9iAw>4JzA)J&`{!p z927kk763`eoK2`&Z|8gzIi}Cfr_N0K-!p%7z_oQ~m?QU%#QnzV z#5U8LzuEC^ey>~7!N05Chs7$5TwmoA!y;n364P9)B&sABIze^2ibwdEcPA!O9~=qt ze>in-)w;{f3Q&@kDy3YGk2Ax(|1&!=I&m`3T`Dd{{mL5`8E^e(v2oGaP5}C}3*X+8 zjEv0NdF$<097k&z=&&u3B@RTcs7_b>DU|(dfeNo_cSitQv(&o#VQ_n(Yy7Y4Yl7O)T&e39e-yi^7%ojjG^@A5B@qvo|_c})Z;JN+@kpv9#UmA)37i`=a#_oSa z(w$-K{(<2Q?rC`EW}adAz5~#`Zwgxfl>GNFyuScF0I3H60RL0te0Q=YiUu_XCENVF z1bildwXoQND$Q`5G3FYrsT-w8kZEd~OG0c)}wumc#)1Mn{u z7~;e1mIM%^2?BseL1&+(pp9(=^trX?x^(ryYL!7A0f4In+BB@{TBr3}jhAHa$S_k6 zuejoy14Wx;rghxq3vQKWK2Y>cqCq5+n!&A(KJTft*Mp&vdMksG_7;r%M}nCpVs z2T8V4c_IL~gYP{a`NVBm0bQ}no%h855hl z6*i_`9R&%3$bsT;q(dT+?|<3ypZ|u2{NFK;|M*xqF9D3x|8atl`9EVG|8cEw4yDAS zE&nlw|EDp9w|i);9Nj$v7pVu=ifLE*8%N4OjK^G`T5W#FmXQvJUoRul-V`^ox3$>X zp?U51P(|V0W}D>0UxeSvCOqj0&VYYxe0bpB9Kic$P(Z}IS9Oi--9&HeHc(xyI}|OQ z@?*)R9S^?)UYavIfbdj6e$DnI2SEP~l0oMQKlu%}PW$%W2T-Uc><6k5+VhAFgs%ghl1JQti z{0FMQ2IISLkhPFnfV&&ujasf%-i$!UwSOYE(^_6G6`;($(E0By*XjW&7f%+7{3DBb9*D?^Rs`{9-pVe~N{yDc6^+&io>%aw&1g zsmn_js^8XbUxPj;lFF#4KpD5AvUXnv%0{m#>>_ zG2B7Kq3<23$8Mymrk3-ym-ZoUb(yIP+;=vJE&75`;f1&*b`bDSgiLNMyZ1-h*|D}} zINSj~lECL_ti+>oPw~m3eK^T%PXHw7bgqBB_G=S^1<+!m2)mWv;Wmt{Ja*rX6C4EB{m4S2co~C2 z6=koXE7frTG&Dm4cc?q=HoW{%0|(0ff_3C$2-1D8mP^xxbnmel&24Kqu;Z4g6uAwE;X~9_CM%QEQ-7_J+AF?2`;g0E^cnSfczGDj}N7r@roq!@OO`sm*e><1M zuI|wlC92ozF0B3>aM0wwnti}DNr^AF?>q^p?G7=!ZG)jT`K@Vz*MuQx(qL7#1n=Sg z@?Y&}hG*>8*iGOJ3(W7cw>)T5dn6I3JsorKI5A#FT?ef>D0l<^c zy*)+AyON^OTk>Zpvjxoo@FdHm{s~uUo*#f#Qn`!UG^U?d8*t*eOL7iu3w#1|ad&Yq zh+qbmgN^sBJzX(#-+t!A_RhPUPq4EQ#fy^{{n2 zkJ$i#xM;xeiTMjNlLX(H!8PEO4c9Z-J4rP(nGC4;Z-5pG;t3)Z!J9+_!^XeECRe6Z zhM?#6D;ybt?!x`gn!nSfev=Lu?cyPCYC!h_q52zBgT>9565ImU!54n_<=FN0GOi%} zYTUCc6Kco>D);WaC!E45q)}W z>+J{~=EP;V$trP-~dGe6zJr@8DL8EXn3X-qSXUVV}1g^{;b?#oueE1DveqlB4 z;Pbesy52?SrZTh7qwlD?%t8|B-n^?!LMnJ&+KLWgdQ4=+_zc+bcq4s7B<5L>bu`-XGn z1EvLwqSFvqNsE}}7jfV3^(AeGi@UWS%qohfcG7uHTk!042-yFwh&{Py9G&JTAUlu| z?&15%gZMkxm$>hb0KXwGEG*M^L+igq+-qVn-VApA5nCC*VaypeGstkzI@ zsBJGa;cJZHl;nazYUH8?)9&oIP_3vC@2uWiz<=E~%ngQzGB%gZ>N2OP^q3C5b&-)Z z-*P!{5DcmM7%_dKVlv4y+J(q}u$vP3L_`5Lc_qkK_^uP{)I*OCl)65iLp!2yjQ#JL zEfAMydjuV}EiJVrNLdE%)(H~-e6uy}*VR_f>pIDb3u|4s9j+3Hx7}0-abj%bZem|2 zS?S3k>d#Qky-%tSYujO`0lo?aP=7GD95@Z+s%OZF)g?$T>dAfHUQFa-%jL?k@|?q} zAxtW3^lP4L;{M3KVa4h3vh9`xl5D#qhC)wjx3|d1CojVm(<*0ZFxE`3RDP~z3Vz@D z&g$FZlMJa-8~;;@LMnJ>3Q92Wgs>%w9&4h&Hz%5Ap_5YcjO|yrAjDAiStAm6%W?Ly zQs_%!+*~=@eVlnwBp;F|7B@>DbQgA?H}x(|Ssunh>GlY@|FYee?tlkGdlIbuAT$NN zKRM?iC#3La%2l<$)5#jCCa-9Td zsF$Mx*-a71DwDg@8C4W`s@nZ1xg^s=YW=mvoO0iL7j`-G$v(3n;ih>IzLlGV&NdBz z`CAKJxP{Hw2LA*(G&qRMe_YzAucJRGY2<#Leh;P>pz2FAC{@CKC57@s@zdlI&zdW^ zo>@i0^|nujf>Bv(CD(wMnCka=uVW?ov63I^bGKTbLhqVa7HXU9KkJOA{~fAg2xiR{ zNS~h6shCc@ii?a1;WeHAPFksZjT>l~@v{e$x8myF#GXzSdPCBbj50C}AvW|&xqCS? z7n0kVQXA`2_{KRI9lxoTGBrw*U*Yq1ZnQ|UUrQ)f=HMdEVO3FJ96j=-CFrL0^vxB- zi}yZeA$2x0J6k&o+FeWfqr*r3d3HVU`H2UozwBO8?)H;yUBdCCM$+P#nQ;gbtYkrb zt|N$UrFq4O9Nn}Z?e>&3$R+<0rxKNO^QSu)EziBrCC#<|8z}8u2W72jNoqZGQU1-x_T4i<4Y}S5k3oXs#0{yR*0b(`XxvNR-+~; zt@}h2QAcsBVQ(?*RS<;ic+Y{e{y8Hz#$;RO;8ktS<64YHDFuG}o_s;8_FzuPDw}85 zs=u7XaJx;;%;7oo&c)sS7mmRtEcxAKTI5(yO|%^GdYw-?=#kNt)1?l+U_eUlzeRqN zhdHyX#@4kJ15QB1ZQ}97o4-bS@l)%siA^MTSv2wy;~uIpx6eg2C13l8;Y6-uW4ern zcN<&ACVQIy3S7%fcvEt~Gm^j3s@`*EOZ(_)WtPsRzx=PmTqxnf{4YkJ!xp$5pdW|5 zcseTJxF4YZ%C=N+ct!cUCB!i%%U7PgNucw|uj!Ju;u?BB#|J27wG@HVH`R%{+q zQ(JZ7Qc#AD@Gd_VfefF`AX9vIr^CyKJ7{y#>8Q8JA}=p5O^|v%zk7AMK@4L?*=*9t z5K8)oakakYHpA+CC+`M7^z~uYn9r(s$9z0lrS3n|@}yWyyJJCvc6a-+@JJQ0ysA}$ ztu7K-1n13xlB5UMM?fDlCvzGtm~jIf3uz_wOPLx&>&Txt>Z*_2HDzTW(#e+Q_bF(y zF$w%iCQxF3jkfEf1+Vs(9;wmHjK38og~X$$GFldJJ3GRpvjO_@u;b?G$)ckD0k@|; ziem_<=azqMFe+ikh@Sng$Z=p9H--WajdJqct4d^exHAhMCu)ejd=vkv*8QF&CF9Mx z0z2l052p$CQ5_?N=%-oP4vwD1d@IG-L4^imuTD>*s&|GzWxku)R2>2a0nVl$}OOyVyHj}`|63?CdAO1Lh6s;+*}p!N<-OL>G+=I!Ew z$0#P6vb3E{PF6znY7l$V)7W3ha7?H9NtIyRJ?z^ilL8AR?4>Kv1WYgQBDuLu{q%dB z&yb~(i3rHITGF3jR+&>XzmT5c#Kl34p^jLj=AJp=2EX>R@+65t6^HsJ`DT*cT|*Q@ z-4`%zek*MRzYyC-e_oK^2j$SY^7NG~W1U;LO-LS~9!=j}HRp^6F=Q{*f#x}o+gDM1 z>+clnKoA*eq_S?0VAum0&hFuOPvl8$e$t-`9haA3vOgXgRn>QXa2|`9dqW`SPc=Q< zjIQOsdgFqw42Y|a=-Y?n;b;aKwxgkPXBt9E2UkQwjDvp+n*O560u4PMKxPT#>QlK}VYVytg{s{94j5gfQilY$Wu zcHC~d8kSm+gDA!{r-he(^y7=b`RI=*#yXIG^E+xo2OoN(0Us`6k!2{YLMd51eSJW? zOJ`kS_e}TH_52%m`(kMb_`o)7_G(Hq86sAMgkWMKZ8!fbec-(5{lm?xveJG2@FV^e zX+JFvV~{2r3O??4kC5=^k4M{|=ZqA?_3BkR{RM+6%VE%{!ygHHul+c;R!Q(`4B>kN2Gc+<%5|RURnDbLQ+^{1ZCI7U59Is2ukrO*4 zCn~$gmJ0cdB#iYMQ@Esyf4?S1!MB8XMj3`5-jof!594q5m9HdY&PmI>Ya0GK zdbU}Bp>7P!xCLrbc2r&pN^6yyvpxECk^c9NeapVFiN6XAF9#vJ&hdSigdpdP?iyC+ zcZHtHCYO76t&}{|?;JS4btyU7bdFVD-$G?h3q4MS5ag1htbW9+PG?6Txqt;X$@=P9 z#e@jHfT;ec5w*`IY3;th7i>9&vd>teiE#A4ESg$3XOn1zzx*|LeUgT!%69rk4Waze zmg=98{(NtrPyf1-iY|Gk<3n~}t>E8jq%8C}d*;P-#YReM-86BvaDR*z^G7PVHy@Gr zF9_R9=QF5v$~}=u*I{hU8{H0ARGb*|E*e!#RyZRo3i-a9+Y+|G zUmN-fX;g>_^m9jjJ5_NylNXzVuV}I&zsdrMMP+yG!!=bDrpx=_vl|W}LTTEb#-PTS zo+6x+LLqtXl9||x4CHlO2MIWXiWF$iW1#I6tt)VI&qR#w;Xh!-o zBKli)Lr#LJZV=U1k_0Q({m%C1?T^RzcAA8u2h#dbGHPow3C?cdxuo{~`>;icMoZa6 zs2CxzAsS1{%2SRF!*Dkw3o|ppl`|cs!{3K7Petzu7<{UbaaJcQG2$h}U>%8I@@8^U zB0m@jQYC+KycIG9H6NKZ3vi;BY?V>mq9VGC5zCSG4up=jd^nGdK=H!T*FjE=?sa#B z{U<4wf(l&2tYl!Pt|*IK-x$_~&Zx>FWIQ;xH||!B-whk6L7RTPV>v4@(O|s)Ty{369Iv zUagYfV0+<{{uTXnaZ4bFnsPZC(oL4F5pU+3dWYS}*EhzNmZgOgDU%VzK&r;B0P?cp zXDY4$tGb(Ok&?1$sDN(K3@F(vT{TPOm~yFeSozZj_%}CXvM8MB-*^G|B&M`uC@qb8 z^t_WE96Z@~uZ~m2C}TH(&(sj}<`TDxKS9rr4sUNZV*3x@Ex$AW#}+iFLf$#8Oja)! zgiMMFsDy2^bZgj;=8pmfmar&L%VG&aDe+_dg~L(v@E2966lX64Na6It>TSd2 zVA-Z;nVhOU&1wm^H7%4(IPFEmY)9;-Id1UenU=E*)930D%=$|5z8|@-&+?PSHYO)2JLld^g zzWeR*H+e!0q4aB`>YjYO`f?rKT_))76S=@P*#`G=RmDsJg=naH~&(*Cw*Yb(%e5qpZ`?nP8kgU(e0Qo8qD48zy3#W*gj`oe0a}THfrSj z@m|D+TeNom5tj7BaQrR_j$PgF-K8l|Mp$G&1=6!&2$qW{?8+YewS6Jg;KYuwRu zk;pLlt$NghzfUUohuw&oo&r8-H79n&d*u%GxEx6;dhN@<^qOaUAK(o2`E|+4GRaCu zGCIh~rP~61aNs#1`@RqUPVxD@H+SAC+APgf)!s_>e_pHps>Gk-U(alvujqkwu@J}6 zkv*YPGS(&Tt07Hpcd@FDJn0TH=`?5eCLfSu*KuFnN}qk~XwD^z(ECTzb2lYx#nWhx zkn)}HFVI=neOSh%ZTwtSR)_`zgk|=Sx~B44rPqImrUQ}oJHh;oNk_1BD zbsHi7BFJL44@{r&&8_tP7>j1h+BKAl)6eF28xExG*La z%UkW4buZkSxrnRP1a1E~?BICHU0Q5Cn?cjVt$GR$H@k5(cM>1pm~_}>K3jO{`~FGE zxa1c|IsETB=@P_b!cFsaUinn`nS?(;#nTjN(C5iYNP@z zNFH`^vNB}}o71n6yZVE`&ZC_7PdvT-M=dfT^c~h^wMoR6WXmFde>tOLx1&I9} zA`W(he_}n_n-X-x@A+Cw7B}~Rk8-7Tw)K^=rdtjjB|TJ-RSLjN=LrRlnHISMje!MS!bW)K8Ev^XjkXYDx7D)0n zN;&LBxOCsW1d9xiyUixMadk;&6U?Uc)N%G1C>)64ZnAkbpVTWA2uX5?~ z5@!`>XrQHH_NW@x?JeHhO|nIZ_}@fZj@uzfh#dGrmvrKW;twzQPWb`GPcRsDkJc!B z(Sl%dOyk-aNvX-`4e>Zm#?=a$M%n&}rCTSy=Dus!bpvFZVqSC9ZvTG#s;F7H^)^u% zxwKC{0VMmW6mc&`oyx5%M*+XgPZk$IixwBW-JN%d77MJ$Qast9LTnhL#Ka~Qf0#3) z_FK$qw1p#v5*O;|lh^I`Ywm<~VQ0%$s^~!vSi6=uETAprM`zS4)gOuohu3JRQ>REx z*Na4-&nKtIKdFlgvF_H_@H-<#xLed(pUbTbOL0~f5n?9a_pRoTA(H%6R$AU7b36;} z0aMlXPk9;c@?s@(>jgOfLtSN8O9zz)TQ!+Oa4|WuRqjM7O4g`Pj#4RtYfSQQ5~L1h zn0wRSSbW~eAd@*J9lDf%P1l2UGBzbWC~kgz)sdw;^pvHg5@AJOT2q!_|NA})^-2B2 zth7Hy^;NEGsT+P3!pLy^uku&6>u4##LR})R46V>x)^u@^T62!E8~wF+O{vsPhIddZ|4ac0guVCnd-4 zk@b8UFEeP6mk~Ovdb#&+)jz#_uK{Jr`v#eK5o@)v&S&cQ+N}|dmi?N;_x7DjiB(m96()|`O^k!s>}sNYg-rTCv6+IBCu zGzbij_2w39!9a@HNdBO^Z!Fd2_lSI#Ed&QqlL^*qyOovuh(O@bdG{-DKKwv)1b%)P z{1!M2o-7}F%rZNs`go71Cu_OINky}j<@~sJ<|%NDTRzlHKMS1x`eY#uYHM!+eIw5M z=JwiiuGJgEH}}cGWt`0hIHpbym*GoC0w>^HImlJ~RL3UqB&(m1oblzH1%`p^M*x3|s*K<$EsuG#Bef*9C;bC!4 zoycL+z9vjLWy|!VBwr_%`HCGH{k-;JV&dI5YYQ`5Tb7~}Q$I&sg#fSt&l*M$0IK|8 zqa-yFeZKVcoz8{^W|m6kO{v2Hv8#(~_xcrP8_VPe$vkUnzp8mnyEk`8K4xYTK9Ap# zsgh(y`3(x(>lJ&uj0s!pjWO)+Z`f^s{C#wRv zt3GV_!>y|zH?!DptuGsdzK$l@jt{4YyKyLy0MH({=Df?Gx|S);_Y~(Y;_zUgGhcE4 zeF7g~1auA<0uWCBN}L0ffSmt6(FX>K+}&2?NCA&7a?vo?wVQMpm7qf8E;Gmo z08#A)0O!RzKo9?O!U*SkUgzHuKvU2V80g{up3s4v*XaynAqVIHfsSg$6aq8%`T-x{ zx-hL`B=BnxNDskR((oCBSSy^S)>%3Sa4?6{1Pb>ww1BiO4EU7^q*v0;GhX7pBhNB& z&_i&bCm@aQ6-eix|Mt2EL`y#(=0_MyD}H$Loiu?D=r3mf|1$vA30Nt)Art^oS|M8@ z1LXtf{{M3VSn2aRb=Z1f(Sgcm38VeBBnmki@}#8OD#nnRa6d|=B=Tovj&h00)t;9v zo~)kwT>O9L2Fj%tAnMB*>W%H3L;jCkw8?>+izho{mp<^ys2~w&+E@u4gM66-;1iHS zeFUJJOV!es2mox|Lh(?^gU~Gy5^(-E@wDbiJHd(8ecsp8;};*httF8f(FA{)rg+Kc zr!hE9NTs7dK(7$hc7+8OLSw*~}}7^!DSiKMODT0geS@x1J-lel1&(4GjLZ6?s15TBIIobTK$PIQ_18$bce5vN+ zIZt@;L2d9qXEFe+(to`@TM58>5`f&pmjbuI5B?UP1EgU9MiS2ipt(O|6Ux=^sxA2T5|4((mfb5-(*7~RTlCb7k=l^*OeCPj< zWBer&|L4EZao)k3|LhX*A}EF$FSmFx1-KS3?7+|2Es*m++~OPHOMwQaxc_@A9av`# zOPmlTfR@F%8CSixpfM%RiGKXtk_Fc^#3QNa@kNaR7r}T$<3Es2IlON(0RM94 zSQAi8Sb}NRA=meYFLS8Q@SYyus-abs2aEQ&dWSw)LUBsdNx?lC$h);M~$~yadpM07I0zGPlINs;XFg zyuWs}?JZmAtOJDbT`xIbAkUoC)7b@T9>iUK@2~VP*@+2_xT*x&lE_hl>Wd*3JfykCFdf>u1Hn`~Kx zzAkycj++;gG_;PbOiTMa@^@SBYOCgWK|Uokw#g#f5Mh!9d@j<3s`Co8&fb}DAtL=$N2!OTpQ z3Y z&EZ!yT;VL*%<@hQ;8X4_L-Q=-od#m86!6Z9pF`l|Ge>?OLNL{m=Tl%iZ-reyJ##w? zbS&3-=AG}#>_7bo-9PJx>0gV*`R&XHt$d(9UPl2fD9*j!VJz_e{O}o1&NI+>Z+UnX z#}$3%dzOP9{oMI)mDj*1do?XP<$~{Q6VX|Gh(Djb!_3OZfT%Q3CA^Svk0MwF5D&lgKhSq?PS}JsCb(@$Mw+#X$H^F)0+p1v&Ns^7%zLytT_E0j zG?~lNl18B)T|QA2nYM2s;bIX;Zrzvf3+NXcw|ue_8tdA>c~hI|>&S;3o*11@Nw300 zG|Rz}s5`0A;qHt4tf7uJuHLi_2@Dv#qibI)5LHf>e!nMg=tjTQ54FeG^&PYFhwE+k zaT&YLy{;P;Mezqd3*U7abRNF^Bni6}4;vltSOLC?`O882DzL;6trhp-!F28AhqQ2k zql|_cveZ3ObVMwyufC_k8!10S=V6Oc4F7+jfQ{k0Zx+f%*?=s3w;R$co`g& zMPCi6+UG7O)mZd#5l8X1;*u%NG9&P{(j5L+siSf4xS`V{c~_)l}Q zd}|GC+ry`JGa3g@k4f9dy_CGgG9c0ZUDurWM6&`Vg+U!0X_^V1tRbaD>t5UXW@*(iV1z^m$nFul@3p(C30(LYyR!!-K zv$VAx%0T^Oh2-6WPUXRvz8voM{vC!v7)Pmr5U3RRGY6ZTg47)9xl9`FcP9+z@R6sS zkTD?%Trw{0+0t&2<;tCaSl1g;90F)K20xLbsD;$FO76!y_})1EPF1aZ$(`*U)X**h^!hsfVt@3^vTT`CRQb(QZ2b@hG;*Y);sLA z?_8L2XZgu}^&1n<=L;3R2f@)3KOS>=no9u(j(597?j_oe-G@n^(BAg94V1ISE7>wPk`D0T2?pkJ*3p^3F zg<3m)kfqi9DietkcgsND#a{r~PM~Fr(rcCBf+J||8<)S(fscHMxDZ^dl2Yv+-KJ92 zk`hVEC{(}Vb;8B!CV!Mw$d>1?Ij$>&pZYV=c~5VChCUM*r+`FkKA)F8WTB*<2|V%q zDYl74bXUO*9^9_E0S^UZPSj}QyRDWNqH5BD*7OfKnbqD2V$2tq_5!**5yEZbmMPm4 zZK_P+z?Xn3VKwY6v)^ZBG(DbPmt9Qc;$i>kY1MjLf~zd~7~(Nq&_=A-Sf+OCgNCH^ ziMps-?fC683q`z*;@!K{^0?JsC%-!!uddwTgsJivM0W`Z&TW9b{I63|UpaX^*FBtd z<%%iI1^G8^EPCJzQ}-wf?pu5|i0P7O54!*TF^TI_)e@q%A2ez=d`xUV&b6{4uk&Ak zk{2n4tD(R@Cac6chw~#Y7B%p_{4KgCa_zL@e!h%&=>SH#A{a!I;s1D+Q}M@#RtF_ieUCsu&tA}HjncV1Na~a z!L7g2(XtaGO=@6LGJ?BoH)zCpY1f(sND&U=Tvwvm{H5yYm(qekKj_sm-s^U*sBNh;42(V!Q!*l zGwzmh^EO!!gHNb=ufo?go-d{DwVRHR5xnj<4iBREFsPIK{+1B~B>u4`wQkx8HN2Br zb$cxX&A?1~X)Evjneipw{E~x%3@{G$c>x^PbUe8z2H052>g6oTgN@&e-LC>S5sN@G zEFf2(rmlkDrSWAq4~OfqAidoD1_@+U7!z}_$uYO|)nOT$iQ4zJOp!t9s%YCiMOnjo zf?E|~CKqdeO=Y1OI4G%SB_(San;Q6A$H~G_wl9AB|svq?A zQ;|L5f?ejR-YUV9s_2yqc*E6mw%AWXy50WW-~6UN+9o;)9`(SKDmRUKH(9}gZ}r!G z67jqKE6FM_4Vjq5`i*H9Cw+Qk&~?5;7QTldTl-ao#<|z#CDvx#^eM72yBQbQnh_;~ zaDH%t&FYt!Di?5*8)5nY+*{e3+T*c-E*a!IK+kUS`$>bf+}x0cJSM8h56oVz;{L2$ zm-=P5pmX1gE7H0=F9axP$sZt~_i_9iO6NwEXX02Wz89b~`eDlo?9n`JW3y^97bN22 z)b2!B9zpS;ElGmyBk^alNyV=)z=$;ztg7Xpc-`mz%;w~TPh$5|B4dYb8*ZN-@iu2A zc+HhPjf3Ky65Ycud9015O8cSa08~&9{uR!#1GW8MjC}_&nB4 zn{fuml0L5S%?DQz^%sm3|FI5hKF!&@BXqp_X8x6T-c_A}8dH9%M@{)PipI^_i>V2# zmFob%S~@P@o6f7_7qAKYXb2eDsjA|vp4-`8L=RikglE?ke!O;zMfvUq1<}+Y>%};U z0ECdsozC1?Xz>NZ#9W77|6fPezdT+5KeF`+Swkx<50^Si z{Adn@JeT+kka~*OPO>-SiHo|WPYDh)X&!QNvGo*8ttYaD@&grCp%7N?dsnz@vpvT` zMRX-RQiaS*d-|GsVZ^%mjGADyDb~5(ZVJtaPnQ^xw!oB_b$WrF)y_S;M`F)2dKk{oKrepj;CxRkA{y=Udo<~OyD*Wk)oV|Z+cBo z3Sg87hqRUCr456e?Lic2)WxUJE@>@3L z>7${(XCwVRr^&S}Z(-x{qiilt<%{rPVt%53ZC=kSk@?O?75oX80R*G)fe-Wb)5XS~cz3=L%G`NTNhr~Nr_^v_?y}rq$LT8` zmtT(2bYsV_1Z2kdXlo`>)?fA~-~DnJGFz^MxXTZyZ zsHPM=h+?re0+EC6N|D*;PYsnhl+XLdw`KM-;db)pqBW0jn1^+xOSUspElRPd^I*HN zk(_TKO4Oba{LA?7iJ(E@#8hX&NTYkf%y)nlK8TWS&%XZY^Q)JdFrqvEVybj_r^hQz zRT$PB6Fe~eIFOmo5KA7XS#SUsKu(J0qzh)MW7uHo24Wv<;QAeu^h9$n%k=cyJl2-= z{RYGa;LVdB6l3ZRuRb77i|e65NRgn&kfvn4&09X9dsv?d=ayiCpsOy+r!Z~g9<`M; z-=OXU0kBHaCQlCeI`1_iH<;!44?YJ;Ngz$Wu$%KmFSuvIwH_Gp5kgz8+#;<>q_T%| zQCMY}%xe0g_-x(7v+a*3sn(^EKf?!PEq3L1fPe(zh?#8xmE{&Yso#R;0_ObCAhBg8 z&D`NkxGc|vQ-lhzXaN6EmAW3PoxL3I=CPcHoG^VS5ks7O_Ge~3TyVj=1CJ{Kv(;y_ zN46(vKRUMiy^tD}TWVf9ES20v(<2qJTj_l&9Es_LbmQD$gYgK4+Nfqk20EwGY)VC% z2QyJ!8ppn%58euIb{neshKF+foX;>46`6LZJRC)9!Si#NYAIXBVfwzR4_NX+I`h#q@p8uyupCu0u zt_YnU&}>zBxJY1S0KLh-zbopo!w4Yl6 zU*dTNxz2KdgD?Q#TWO@3Y&5Rd!JPT`T&@xaKQhd?2jJC`lm9*FrbDme!T%=u=fwB! z`T))T`#)!Jsp~U1(*Exu7j5MR9M_(|gL(Vm%^}+)_xf0FZjErG0Z?>N=Wenaw3y&u zr2iZz@}6Ex0A&6-#pEepr|YkwU4SF^%a_*`DY&8VU{FO6D8p9AP!KV}A<8pO`q?)! z4UVSci}tf-YgFibL&8RA({P2J4uC&_l0z)^Hq%bg6UZ1*HnG!FC!lHjmzv&He*#)(7`%tUdZXV~hc8-@~Lehr&bmmK{)F z(D$FWP$Gp^NnVGhNi2t=#D~zW2F`@_k>l)`n>(4E_FuTW_8#3g=;9`zXpZ|d4AHoj zcAS`#z=14)cn2lr0h$JU{%1D7f|1UA9wTt6PtJ>-Mxf6@2ges8SOI{DEQSIeG{XzRqpUG}(-ZQ~EE~2c^@pQi#X?kx*ZVLe!zf2>3hTzkY30>65Kq zm4?MH_=uhgKa<}7gY*+gN_a#5Y&JLJ{TCr$l@s)JH-CFM-io(bbp-&_(9jM(7q9^i z*ucM0KqEDGxxbV=Ju#uBudlDCr>;<3RP=uJ&mU_u!vxTr$E&Bxme^I_DVLH9(E&Xr zI$Bk~has6iPtLsIbMMbiTuN#r_I>_Qc{sw2CSPKhK++HUFD7OzAGeQH(j`eW*56AK zxr*A<7GU#v1K>CSl|KrtFRFG+&0Xi`e<;sMUHKv|JjH|L`M4Z|M)kea;#o4Pob`&a z+Z%x@!*+F`N>5YOB6v1H^o2tnmj>6qtSDM^Q&%%3kha~ZSyCFDv@w=Nzku#)88 z&z{|gvFBc(IHM{gjY3>ODf3a=eIvk$+%?sW3} z0<)hb1@9R05w;Wj%{F=hI6+_2vbVQx|9ni{Oiv9o%>7g6u)QqAXgnrRA80V(e2*qB z#aEK)DtH=NcuJG{Orxp#DF3^U1?5yJoYI+RE3uCs3}m{d)i7U)0LW{ug{?=f)1)Oi zK-=qt*DOXcJ5Ij?Eui$t^9nf6}4$voP z)Uo#bSH2JcK*fRhxrWFXR7i|aF{+1on0v>IFu5Mdn(}I*qinP2Q`ueyz)?VNTD?sn zOUTaV*+N;84kMao{I({H;Wflq&i)bx4uS~sIGuF}sjBp93pDuLh>cW&W<<7wU-hX2 zvN$(M5~NM=j!7|LmdfSR>Y_3L)f&;-oNO4Ue^0z~bTqIn3t}Zl-6=X8UZyxP`iBpE z9jC@;(L_%}qr1**ZZVl+snLQuP8MQ8Ns)XV)1~C@-%Jc9dJI@p<3B~V)^aSgRIOG{ zA6ZcHJ{CcTj5z=efZMrq5%>beO+I5g85B28#GK5JVtD2 zVQum1uW<-2&$j=r!F>u>JD6wT!Z!mr27q+L0l|SZadnl?LhNff(sCgj zJta@tS&oy_(bq-|HIc z($*8rXqKjl=~hpbEGBq?NJAx#7Mt}D>S9xEYMY_b2VN~|8mkx8ws&QyU7m44hrO}Z zyN->?oG(mwJYOUML6wliHpn~M)jd&4hxoyHXF6Tg+h)01nldsO_nctV$Z>?;bd%3t|!1mktzd;D2Axh?bi;Oc*&3(yoBihBf(XRdG(Ez{v))OktC_QEX zge397tb^T_>s1qrf#YzQ;%B4WOaR^mBA@uG5IKpoTm3jyY{PDK!OfA^}!?V1eNVAeU@f7*NG$7mJN{)gQZE zCP1tbHv(<9hMugg)$UzA1B=+)^My=py^_mBmg^pyB_0)|^(ZP*=stS%J|*3A2c*_-jWxVbG#)LGdAaA0JINvT`B_~6QPJQwSdH7`(x+HZzGClUdt{- zk^>UO{5p!rH91hlmYvE_cJAQ*YX;KK{$$}Prwy}X)z00cOoI(8|1BCo{!ld0Gtn~_ zTdMf9bx(Q6CHU9r)D-PdS$6@rxtzT(^*9PDpE%V_S1Ec>99I+}`$Klx4IZ8jUv{qw zGH1iBv|O@DFRmyu`U~*_yl??fJQTVf4^K*l>s@9cTb-%^122L?)8W+Q&a`wqQzMK! zq-lvoLU4HM)y%qIkW|_E~>*aJM@C~0yKSSP#jltkT4K= zZ>SA=cu>ItQ2O5JNchpogrwZI`6q))Ao^r-7~x+Ut5k;e2+4KI0RQ=$H>obf@{gxw z_KK1M9vx*H-6{QS)U;tfT#w_gFXeJEVSe%bWFkv6Oo;Y@&QxWVb}ug=0Fz3o4$D&f zxg*_^^}$l#nCpHd6X{4*S>Y>Gyap8Cjc_a}49A3qgoYH04cm*gch!Fs^&W{2kncFk zG0ID8@YmPqa#Wt5E$J&fR-Am`-tBQd>{&H8$%$zEjU1y82ZvQ0RDsk1`t9ahuv?z5#$W!2`6B>$AO5++2>XwM zXUq`OdJ7A!LZ=MiTOC$S(3oTEdD2iyjPL9fY5Ixo`dn4%M1V0Nmihfx~TE0OVu6|TWS#^G^(A2$C|qIJ5uL(@Ba9q zph`A()(kE#Els+{aZ0S%8?55L2~>gBT4bs>H6?XD4jO*I^vU6XxeS+0GbaYD%OpE^ z5$VWvs=^}u{jsSE*L>pQ@)8@7_0lFbrN zc7q(B$VvzQ03W4||F%ej<@bIcL1c{e0{%GpI>EP813;51t5 zcbQgyUmvE@fwsbfwv46J)AQQ8Up2*Q?!^{SJ#_**{LNaP1F->Ob=8OMx{B@2uCYz_ zOPy<9pC6$QV1>_3O&Rw~Nx+Pll~b4R6Sn(`ApRTzu2Cw25#`Rl8J5ANHGy9Pr|q{j9L;P=k>aFa&Rbr0QWqrk!HBR6 zFYURON{4jaNHxl~nBg)>mkqVV!&>uTbMA;){Yr8jiP*+;4|r zuYrC$F|gs|na4?Cw5kkyHX8@_h~M3K^lLQK*uTJ?BvKG?{-Nw-KrZSfvvy9P6MUw{ zIJOIl?va{t_*US~kX>QjGKqUpmR>UdK`pMQ)Mne$zW)3n2s z$;~$bYJ}I%|1=x8EBjrs&ksIwLAh@qLx_;i(X-V}<=+|SFMml`$x^36BrKtfpF?_S zFUR5ob;F6c<6XiLPfW#vPw}ACywAJ9X*R_ znv1g7BkA>6pV7BjV1U0WsV*k0YDBUZdQA05tHxP+07FwOAA0DW532`!c7%4I+k#A; zZcP}NhXqwJyZ7PmF%S$Zf8@2Kivn4tpN&(lF+0EfN!}+jLWBWfntH7(%L2MZ_k;zG z`4BlQ1tSLkd1zk1Lf7e5?{`=xi>((y8%MGx`Atw2l~<(r^pABBZF?gzZ_sv1+Gq)O! zHZd0Q`*dO$!s1|}52{+?KKsu#+Yb%&rQWEJJ?T#4&?&uDI=6&_hQ))v_GMZ>a+t*b zt%|f!tJHnyk3IYW%w7hxi&me0=XiQ8e>lc3aT7sHWyFE@FmV|<`mss%vzUO<)oD?Od#n` zpVh(n6K!a%mfl;c@c?1SL4473E`t=aN<&35lMmQ0>4)snnb$&3L$u^%`+`r(Fw+A` zM=Ju=(izr*FnEH#LRc`w+Pqx7q9y5BF!vaEQi%3?46(1m zK6LwzCvG%%vKGGSycnlAad_vXA}*?PSA=B}w@DMDlZbO^3Tx9MQIWN&VqKuW;`<&+ zFFs|J!|>Z*Qct%*;idN0sS+^83M#|x)<9!CqIwt>F*t+feB-TrSbq{GHVzZz`{+I? z0=loUxl+R6bPqDnyd;h8Ub)x5-?1AM0}Cr0sXp}u(J)uQ@u0TUrpN_!M`?3BPOf-K zfuU1$t`{2VU<^^@W2xtP?e~f2jc78#yTm<$-?*I5#NA>R95R+vh-oks8ihYf&u z)|_;wOZ>ivp5Lm1<@gp6Z5WA-MzKfxV0KG;w33Qlo((~t9`F6Xk& zW3tJjD!NVyQfZEv_6xX$B6Nn>i)A*FHlxNZDu5&tG{@iTxR;y)E?40x%>67!AdG7Ceh94RM<=8l$6ZJ&}Dm!zYE-R>&zW-WWcD(I5N#@a+e?hygKLf**q* z@N#ekhtTc)8fb8bi~Abxc6)%a*g2E0K`Gc2lZTEBsn_xGzdKl1;$BHd&zwQFQ42jo zUJ3gggmS7e~G~{-fm6P zj}gzy_uBntKkDGxaStN012rE?!6-d(Rr~KdVf>01W;5198|UY1-?x ztT$Y~%DdYUSYT}TJN9_LRogVHPToyDF$&eA`{ocscAe zEWtCDu&*Vp!fP>(ByzqW<1l#ML2$xIsyA2sS23m&;)SnbSQ{&(X=eQ*!nkWdPc`1| z@s0cnRKmCr?S=jLz5jgJZu*PF71%Lv5}u?iN}aO;0Tndg_)f1zh`J z1=u-@y)1Gt!CMWyK6%R<{I&rJuj5o>^)iSt^+w#aV~r!f_}xhlJalDI-IWpVr67}i z8h9nv&(JgSCQ!Sc1++ODCbvg?k*9atm+^-mC(h?tcl%)`9{NA5MQUUqrwgyV)KgHU z5Rzdta1hwgW9*f}0i@}Ty`OmRgW0&rWHkam)?uBjq@MOU9DVbj1w}MRR1?shlxRInwIRvc9UAX^<3Akm>+AFsvN9d)3_SmWiQ^u3I;e3JQ*hVHDl$#d| z5@E3p{FX*@VV8HGql{=*=9`?1uK0aN=?rjA(1VIc2syjeB;CP&!wnq@;g&{r8F{gA z7v@Y-O!xP21Njf_xFNpU7%@8!q5mecsl|lnrMiOcP|q*Q82gXs`@%H1kZAT++TUAP z$PYO^3bpt=G@Mzi&y3nt>>eEEt@FP0m-_H6a*44ytD;op1bs?k9k%5|+C{XGV30;D zeK%-_IJyPcF~3a_qxKAa!-1Xk4h*pdnwJD^*aw!P~dN&EK7-( z`4y{N5?Er#+>S*v5G2V@oUVuwDdK0%pv;ETpz(-(sh{dBj?=2I%w*rxWsWEYkBNI~ z#j)oKn~vg69nNwlkOVP1zG)P^jgX37K@)CIFy`#Y_%36YfV~EgEf8z*Ge$<Ke{We5X2(=XOaI64e)2u7k%-vPQkP>`jrQwH`jGoAEUo5t( z81{Jn`vU8LnrPBlp^Y(z3-dM;rM7dW)>9Y!B}!l3MWv_naz6KX5ob^9p?mv+#_Ja& z4EaqCj`$FMF7N@suK`}WCQ}%+BH)+gtfd>oMgNMqFr;eunEYMY^GWvS_bbrXc^woxSsiOGZ z4YJ;c51>nan}3m%K(XCbTVJ2r*{1wt!c_0dd}qsTCNGowoH6}7*AxHefpGSHTs*Kh zny<$bYE61ca|ubMrycPTvYt%oJ_NTSzIDI%$;)vOnS@R~AOy2ywQKJt4jcNCiZtLWYlVTkBP%dezwP zeN??SzOokBUJr3j!P;A-;e56<<>}@Czl-biadzU6{%?pPAi`H}MSy-nKP+#vv~jz< zNbcS^bfRvm#w+tu6OmQE$rP%8KK&MQwi1$jZ0C7a_fh{p)lIchAMGddZynB2ANdJz z>Uvb!=MxU}%A(oZ%Y4Yi968$8tBtL1*ZX1I!x1y&nANjp9Lho+Tn-!_93@pQvsSd6PaPs(^Qy4 zKt_v)bM*O;;|~_Q)zZ1b4oC@s>Ulv4EAKnq)Luy3cG3l-Xwu`Nth9p*#U}&pC0XCA zM(XzkLk=2qx)oD5yL7e>oJ{7i&2`t&E5Fils(} zV#PkXaJL_PXr#Rgg5`fEZa2{uNN*H$mo5&$9MaLy#J&~q9xU<9gGfxoE*Wu#br1XE zb7SD(uRq4r2gAhDB;O9y(mu}z>h7D4KR7M6cIo&R`SdFSQc|AH7ZijuNL0{S>oJ=w z6>Rob*?+{i2t|D3gr6?Zl@YnA1}TT??^65X``;pOyep)F^FvBRnTp0c}4UE zXUx@s@M2-v_YBSD$6Xj%U>&oN;KRbiM;^utzL(;@z8){d@nt~(VypWtWcQpHYh3zd zVEphBDULKoZ$^kY?ZvKZ@Phv)FGQS;9|;LUW6R{Q=Rua|_4>KfxWLo`(<4V!-K{2)vY zI3=zrA1U_PKqpfIZ{3<1N+12jszdt-m}?LJ96cWS*1is z?e|&9T7u9kx^rL*(2h}!J1Y^GK7h~WkDNX-x42?&t%x0*i>>DJ>u{6`s4)_;BwXq_ zR~;+sNe-Q6rWHMsqP{SChJj>+{_&dgbl{R2j5Q%qi*@vG(w&p0aoqj|!0& zgZg?F$-fmTP~uf)U#=P>P;wy&6Or;1$nC+tATZtp6W%LYS5?m z=cv;xX|?y-Dryj{t}smj2qjAVJ%&ruoiO{XfK2XCPG5d;89k|crlgF{Xd>c}eEO=$ zAyps9gU{Gl*t^t0cngGI=Bh;~7TNAHX5sOCDyQudl5@egSs6crRkiema2U}0P(BpfhZ2NW!`5RRPS+ZGc|7Z-A?783w$ z{o|$JTrplwWa71oEcGwm1KMR$K*#&N9mtlFzIWp%0J3-{&VKOhMyYl1v+G(5UBwx6lrxke`%rzB@`(r=y&*07i^r{Tlcolocl&U z_OapQ+YK%30Q7>|#)NM^6$w6mvTf8^-5&=!%jF57BYNsdTlJOA$ppXF-5+wuBEo41bhMOo@J18i#&Z*GO z%~mf}sd3uTh`hBvRl8_$SsNjuP3|D2OqK)<%%wZH5S}In zOUg40?u&kTAA!lVEH?*O8&ow=3+Qqhsu3uN=oOAm8}CJp)ftPHcXnJkK`E0vPs$B+ zw<;z^wf5u>nkV&4w7F!#k{E#L!NAlTtxs>iav?AHEcIx7MEj+F^Y)~!9#9{syU-=2 z1U5Ka4Tt6o7qdT(mUo-+-rnf5uH>7tEeU8>Vcn@pLs>5jAm2>5iZ++LCmxblB^#>~ zOc>K{h94UfY;Dy|&o;arS;umY%M1rGp#zRSCk~RkPtluWe}6R$MtQ3NAHbmD|8fC5 zbCc)a7r-^Zf57+uesN;lMTCX2S@1bGyvNXv%)fS81#Ci4Mj(KX;AZZPUC7N3*A^qd z4>!Jwfbqz+?Z{tqkVCZ_M?}DiKca|60L#qtRDclX25y{Jftk0R>6^L z5gc1%&4^fLg!Qi;2;L&IJ)H!UML;pb*=e5h4mM&=`d#r|OM92W$9PhlXxC10K9xCX zq8pPyz}9ECV~#cj4b4}D%-T5Kei2JvM?!G56yH}70W{e;J8g11 zf50IXzD`Oq>ccx+#dmEzWusmv^#7~SlX&II9S;CRHs<*a!8CQp`4e-K9d4R!@JBH0wavBh7^><& zC7Z!3anB2`Eyu(G6z4qv6ahtM;Enb!Xi4zJbtlt7Lguzy*Iif}6YU>NUe&%S+vao3 zFLTO(Tf3$J*3Yk?uiT-aJ3Hd`JL32LC^3te-e+|QRw`-5AA9$HUFq&8g{Y$^Ru4$a zBAD#To-iz*n2*jnRi(H$s3m&}vizc^(%0|ZK3HnG|KNurSv?1skE+JBXc^s2eAiP$qgh;OJ`4x6>_cGAmT zo+{CQ`)Hi4p3z0!?9ADlZ&vT|rsA7?y0NZ4Z*7f>g0ZPvDh$=nZ@kblkB31GVb8-WixbSY|F zSf|u<(l%c}tjOz6k=T>rGzG6)z7)v;=vQ~T%3U4?GiEAt4FV1$Gi?eL&%dA*VDqP5 z-8{Z1w8YkOWF5b{vIW^{ib-eBHL|C1pbl6(k8>C;y$eQ6_SJlVUVMO3OjkD^mKn@= zt7f93=1wbjnmxJY{LZq3%CSYl`+|^R?n|QZHnULH%VW;^-TO$yBc!A9c1!Ro%=41! zdYXyWA7gY-#aBf^lkLTR>U0!Y=d6RWp#~n*59LE(-(R)9E)L;3vq~V$R8GrMKCY=K z*7kmJDf3Iv`I@iM!d*KbU@xoo*SQ-O{@gzgA2SppY;Sb|3+)57F(6|m!)94Gl*wd> zu2<0XeseulGO;--%^k}_K{w(UrnL55{#+xc7=?3cVvhsS06)O7lJkQ?5!@I!81!{0rWrSV>uFHS_=%k zveGv9yLZ~XadALgtCbSK3N$kxs<>HOoM5lfGC5Rv4Xp*f*M1%r4aeb z2-Y!h>E;JK+8bgPxpn^LFw>LYH}^|BT7KNv-mw?X?b%{QJqB1_;jW%F2k9l;&QvbW zRNf14o|xRId)DvNIW>)#SepSG)z>R6JfV#*-$9T45NQA*D; zNl7Mm{+Qp;{bwE9%+}RB1ZL?iGuU+s*&-R{p1G2OkxU-Ce1IN?Lqj$Tn}h0}Cf_hO z_fmnzCaZ(}xfhhsqsEbX1*#B0r*$@BX(A_`CM%tG{`Ae4lCG`WD4y`)c-7evy7|tb zzv@@^`a}YkPRsgX>P%t)Yj#3FMkY6b%(51k)awnMSFnpspNuqX>~$SJN=f$E5`fj& z3uTW2LnA-{F$VdG-Rw!d5@m=Jb2((!FI&?%{dk>lF!ypW*VUs*?DEe|eQ}jzD}0>u z-akVNWr}lM#}&E4TKpFqPa!`6vNc{b99TFiAT0C{-8;PX%g3qLtp0j2w1=`i)bg-8 z|1hQna$)(?yf}OY@>CjV;&L=NQ<*fYU+~_lOCFKi2_|ZbMC0Q$)BE^cY42$ zGjQpbd?m%jea4-!IcjElKkF1`G&(AsDP?r>p~GdpBv|i)LZf*Gb3XtSG8lL0uj9R5 z5_ZFz4o)rZe{4TFfE7txVTxM08ywakD%M~FFGu)Zi-qhB+)MA2sc!|qx#_v?(EPV)(p;9n@{-?~O+CI?YX3XUS~079 zWyVkyQeQ~@g5$E_&4f|q=+UIX^^+V9HbmkyUH@s_kwl*T9tm@SeSW7?vaCnNzAC83?SQP4QT7IdGS>VMJvtyqD*)2Bav-_ClOw3o$I>P01ufDSRZ z4JemHL)BsCA?EWBb`s>O<;d=%LC^XZeS162283;+Ytb5_pdxVQMvGwPLU_%Ez~fJr zv6g677YrBTJ+IJr(~nxmszkr|YfdrzCh}nk(-{SkgV2O&aatXYlas!L@V=hwHRLeF zBF-&8AVO0jetZf&XIN(P;?V7V1jm%>$jBj<#A7&Qd{3-DT6fD|Vzf?uX@l*pm`7KE z(ddS2J|d~W2$pJG?%H4{3|l810g)1!0|Dg(P7a2eK9gg#-e&Ql^z^gzAcO|_iPGTx zishQQ9Y!>nSUSSu=PzMXGuGIg3)camn^@`zY>^VV-TqBPA0ucK9Juq5dV=};ipG7g&=0}0%eTKg$&h(?Ekn^}^LjDqi+&KY@ z2ED?Ij5iY?%+}xgzt2A_zqlyMDTMHkvyZbnLQj>)t}-Wi&`qqlbg>R!Niyew=ir6z zo3D_ACCiCJxM=eXuT#?Df@M-2ugW`L)cHu}0`axX0)v@<9-2t;m{f||Q)A+ZaXmxM z6wR4*huo&qLF0HBA|wy)k%pkt8$~Mvj2?j`k<9BHt?4B1szh;KHnrx?^BsXXTc+;$ zd4O)fY;^c?jJ7RvydMpfxJbtqrd9y|pba}mOK&q`@j=7N7#3cyR0M*{1jHBr;lL*L zDMBuOBb@!{A1#ps|GDm%372&0nL|f=K=ioMes0flkE8k-dPKBv3Q-dgAey%<^-F9npAx6lejtbh0VY=eK$?TILlljNz*1ksOWa zhHeH5bd5noezs3Z6}tPY;9zG+4FV&qA70$9-8q)gL_X$0(aho~tO{#e-@Oa{7AO)| zged0$_nqzXmQbyxkT(OsLC5umjmrKe&&-EyF|_10-8sD~HkgAm|6wZ?V08i(0f4Dg zH%`raqH?ZA-n3}4WfcVcxwlH=^V>uSv>yG)^V9aCUbqtSJ-=b`xoHT!i|cDI&XTlF z<;smfHq^kBn0s$@-7=hfk9&-@L;95#I^P8$9^Z22K^vpCgoq>`qerA6@FRioxMt?- zT0b7;bvi-x`sem8TbQ(iGC$~#mPszIst}6Qw17@~2870sIJ|jt;K0}rSU)R7^3;=t zf)uqv#o7W>xSkzPlz|>UroQKitx^&r#6mWrtRhNV045bq$-Dkad(qP#MbYBccc}IA z#LftO)}cmow3q;@ChGEb6?{R3$xWrS!sf++rjY$y?Juv;IDI+(%O~KEM^?1lA3F;i zyk8VPo_f)q7r-hVrjZ>&KtNzZJFvFK;=^^R2p{U&hk2|H-Rt-yYXooOko+vj{vv|B zDN%+u32#}6+hZtz8-}*#RVQEc-EO+-PHSbNxR5rzeUe!eIxjH=F=vhaJ9+1(QYU$i zH;Zl~atHaHqn!LU@r1oX?i#{}W16EiSz0H9ZY@Zj%g87YB5<=BL$fJ!CdPQZw=S7xt^i+hxIeOe{uTqUHLE-jIgoCLz$2M`j zJ%#(8kt?$0KkAk0(k0%@wypXTor&&HOWf7iy zqZBY$27;ap$34g!kkHE+s&m)v2 zA|1p-^Ni%4xVG+@9qDX1T}i4XI{_Wb3V$S?NH{*gNBuuCd`#Un@&tYc_oiOr4Ktb9q#&-7b%r{oN=Zl zWwh368nsOA^RS=vzL4m9q5wt6Z=QYiCwpsW52msTDaAhiQgyYk-xE#|Y-ZwY|9oey zYcQ2TTUltI(d&FqE)?1Xzi_7&&|g_v5TU_G&u1n0#uN4mNvRaLN6{!?VOCKcHqswP zEAsH+L+$`5BbpbAFDp2*z&#anrL{HDIJc%RfDV(3KyVS5T}FjgfvtOep$XoE~}x$4@7PYPatHI_~^Y31qo*~g2B zUgdK4wgt-%OtSaNiB?V6dfG0Z`jpUYE0HhKEWSAS#-<^=9Zmm+eW#-zA3H zKHGx2i^qET4`fu^2K4vII@_}N$OMs-xJkZvOG;u z@eJ;)=Jpx{b?s_(HU^hH{2A$AAP(!e*B%|xyWnr}V#-sFfwowc^=p=H!5~O^;ZC|- z?vIn8Ef#U6IyG4s0$rEmfKpS&pNCUmo7DsjcAp%N1jk!_AevAPM!kNkrLiKl3XN}W zm|IJa>!2Wres*W>F~V*5bNE1i`ao_dd?OHgTk*C!*U*Fxsw3CzD0`Xx(g^aj6o)-D z;7@p_!|ZmG=+&#`uDTcR;Ap~FZY)P*i(MLYC(qI3>TWE%)i&EdD&R`b-spH$BPKX6$l#~5l#r*f)_1<6VBF@HQ4hF)aL6gc<-4q(IG|I|AFQeF3DD2|6}an|JSh~XAuM4; zse*z@y)VSm)CTj-Co3(x`CoKe6q!Xa$1n9b+PK)_uBl1GZl7YXtUmW-MWmT2uhZCo z;Zb7vCaY&BXPC@H10=`{`1WOyVt8fOy!5=Cp2M0{?@HyA*qiQuc*h+i*M8zxF#lSs zaQM_C@lt^Lm7Y{4I0z(if-sBBF=tLkHFcUtQo&9@K!4J)G;th_-BX26K!aYuD3$@X;+<^D;P z6lFe79Qvg**k8YMsx^T@q9=Q^ltwp z@j&1&gTxvC$>}LwSy)<__pRYYwug{K5eUz`Q#0na9pk-iG(RoBcV~9wTIT&d` zI-CweB!PH0a=ha0ZlD^rXO(z}PyqHz>IuRH*-x||?f0}H>k(@VeLxYz%XbX2O+{5^ z8>qRzgfX z1tr}aKYYcse<{36Vd}{u{2nm4;&yy;5rq2z09h<*$EfS~@53G=JqMRYKYRC{2cTNwO~`B-UsGV!3FO*ZSpl^$E^}I*)S~NAqVF{ySg~pM+=;=NY z^n%nanL>__;uP1qYz;~+LT~HEM|zi!sRu8PROJGzOW`?rYB!gsYU1ki23&E>WBfW? zDg7N&OsbV%ct1#x3@cLa1BZ>(=yKYvD4K&j1Y;$W<%)m>>804dB+SIYncoFi(-P@Z zO#$10BSaftJGPGR)_Vr7J$n7;r2VcGPXl9#!7E?ZP}3<(m?~u}a@+Ag^Qj_$ijZ%U z3g=io3!Kw{V=gu-^F)qJS%eeR?_Mw%KHtf|OsSbwuK!Aph-2gtyoE=fp9zsK3)@#q zVO>mBF%~1s2M-Bn!l+N~3^0r^_J4dy&%X#A$n=57{9<)~f$@R`rx|bmWu3HyLmq1L zbW=?|NI96%#S!c1D_t)QKKRZzjm>>4A8g~D3IhxqfhAxtnL=~#Gkn7bTF^bVoTGH< zQvG*-jH^x@EKA8AEPEyYmaivu+|NTc?Jy<;pKA@&^=3xRve|_C5{FlhWJpvLss$RD?AIkA4BN1~J5P1~TkzTNse&LxRLaI}4 zT8RCuy(&KL!-?Go-z_`948UtJyew>>|EaK_(QWgiw`Ng_lq_VBz(T}35fwVoy1-s9 zthlY&pV{QH z4fkUiNliAr5F^|@jE7mjVfzx!G9o7vm%Z**XTqC`!S!_Nu!jL#x$<>urf-F7&?BsW zzfy!Rdm%;(kgOgh`{l`DUZrCz6E)D^k$-aF2#j^|k3e?`P40=_UZ!WB#<=ao}VXa`DNO zRIk6F?eKk){ex}_-V)-;56)GTUD1gAG?~iE&+s_eXXd?;z4_p2MAzs3M8=HDmGA2# z;l-F0esf)u6M=|wp&$@~ZrKaa`=AQ$94cdR&pPNc^#x4oQ7%-ADA6V}Z;7+ju|xbU zKf9L|nRPzK0h5;%bqrKMvy$EVfMaJ#9y#&k(IK)4&}gCj4|mTk5*)TOAY0Zn-j3VT zp3wVC#uoiOj$&?bK!Q2D&CXjgmTsVNyvHGA`2smnrHgd^(OAHj402LFGz|P($N&d2 z&&nbJDq5=0j&cC|Uf23qceZ%;l5>dp^}1SdlR6lPYkJ*kiiCi5slzLaiOn;}mULjs z|LW_y!ZgEHr5WBuJI0NR!@|BE?XJfQ4S9#72#RbfslMT2ML& zB!rGg5s)M_1wu;*+)3E|zWw9g`;doZ^3I$&bLPC0cTV}u)T6e_l&_UkIzOLrOyc7K z3x0g77P>n-en#PQ4#VxnI2u{8r$mJ@LQ5&h2e&m^%I^?^&K`T709Df!!2MIlthWRu zO;2=?V^oAEf#*A&mhxm`kWY2GGgN~b3dFHCw%}C(7-8fI-cJk1ev*#OR<<{f|5`c3 zwLc9(MP0{UXVw&H7D^Vbj`uqA57)9eHnf-0^Un~xE1MobbHE?RD(sFuYe)k0ypF(W zv*pGNbKs2weube*=?^0dFPu#>c7&(CmBhNDV_Esjf3uLeGATCgAfJrUJ4lZ0EfLx* zkdBSL^gUbIR90kauYlsVUfJOuh&b5}bM^sd8cZ|D<^;Ck8o#(x&!?_$Yku$EK8|a~ zrq8QP>>Je$dw#&@>+Rc${3fKlobC&=-A~z-IcgQV-?``z=aW4;JcFxL>!JC5W5UEa zjWxE{_Wme*@&4WtM2^kET9*5|!yRwrH-i;Ea84H z^Cw-M;ho(8S(!#D(5|vi4#~-0{q`*{g)TXo$7%)%om@A8{ECF$ih_Ervd{gQN4wear+j{7f$IW~#s zajD_T?PYQfn~kw^vrW00U*5v<@k(cI-|GO*LNTe zMCp(h1^(@p2KuTd8XM%{Zc{{6S$rtP2#n~x0=S*71<^_b>G=+su1X{0UnySo!jr=v zTCA2f;l-pVs9hu!tL7nZT4br?oWjN!g*7$D#yQ0jdQ58SDSH09vdi8(+>Z%j=IXYP z`ag-3qI1@$0Wph+^0;h+q2a2~rB6Nr#dwiUyQX^8FWP!XLlKp({*$d(#Lg<(V9&P* zEC~@olIjfS7iMb9=M8+W!ZBUR_s+ziQx)65<#>l1S2>|O5@5JzHfPie3avd0L!Zuq z8wNQxi)Z*55?UwEGX|sI2u%x*yIEZN%Ur*BJ*f05#U$OUnI0zSsUFw0QtLO?7K8rd zl>IaELxsyTw32uXYfTE~*hmWIaB2#MZ7v0Kq%g%Gbg~r^-0I|IqW22*dOjQFn{-#{ zLri=siHS3VP75!;|Iu;fyqL_XIA2=+_%C9>Vk~QLapS8wf77EU4$GZ%Z0xo3VQAN$ zA|1}pV!v4mRe!j(%aksMCZ!rM2FM7C077KONPr@EF^Y_D7@z-L{9ofL9s}(B#{je> z+$P~ARc@1JqIwW4JB;M;$AAj>(PsP{g4Lg~Br|ec!5ib^|6Awpi{Onx0MKC)m}PnF z&=0tpW$|BIeA8lc@@S){&Hk`EFJRiC>R0!xpL?Ok5vsa|8&;Zzro#s$KA{VVd>_W$ z*L8~1xB(wvDBJly7AyXPUT5e+BWElDX>fl#y+_~uYDLZ+cm!yJX?vD~Ca%ceGQcZm zzmWY!3;sNA@U*PXvKYX%08yRnph`GEqKHcPG$sD{SHV5cUOD8KSEQ;H0iQdATml)Q zUbH4erBGIaKL;L-?i{JG*u4@-hJ4IjO@9@9&3@#w>@Ad^3KK#3OCA~W%0YXweHERS zMogxl`1{5vTT;6?Gj`b^l81&4czO4309DX>*b$c^r)>pq~qm*n!(-L_PM# zm;8UnDKVXf45#PcAxsQ6C3rE4jBgm9x55T*!GhL(EI<(m=*@jG;b3P5N;yy=YnSo? ze=V`U3%=NO|Dkmxmsfu{hM^iuGS4gW zb5kdW6KouYrER)e&Y{^qUvS!3Mnf1=^ocAWS@GT@^%CSC)7O|Z1Bq1duLQZmFc8Y&eJMFmE((=)9s_QXYxceUPWrw;q*JMsFN7^oFLvS758Cav2qz`RSLh5asuqnZ@c1ky^W z<(QeJl~D+TqY2)?^_X5A?iQO3v;9JDnV7xig&cpLI+}4MFi+@Y(j#-3H^XXY&%Ft~ z*tRk2C5XcUgj?&q6iXSD5Cj@`VPk`Wboj}y3w>yQ@Qjapl{5iki;)Ul< zt(o{eS@5yNnL%$^LuQwIh6a6Wx<-lEKwjiQ9wfEKbQIR~uu5-NRq?$zlr86R{D>35 zse}OWK*@&qT~mpQtS;I`lb>H{!dv)2s7F<()5_&=7EKm@Iy1y!g7;Q0tckpJkHYtm zLjLtGI5M)?Hj#PDOeppaOJ9&(xJlvDrL>P&&|FePn2=|x z0RH0-MnHTRAWy26yde3NmEbf_fc&bfi;pZQ;0zg3MDgTSh&~?BJVZS(o7XaXovWsF zzZ+CF&d#i@b4*ROG?`qkcKmdvZM>zT+Yyg!x0`+$UXFaz5<{M2!uiI<=hVguKQxQX z*Wjz#-yS>=gIxOuV=;2#(3J!6F|9i2of^t{g=cJ~zP=KTsRtQQ&|H0kl^b73zj`O(0Xrlc?*@{6a z6M*m~sulJ(92cjY*mmt98g9-y1|uA_(3i27AsAGM9u?+^AMA|XQPp730idGSM*CdK zS21%CQ6xW4{F?OI6{W}_%W>S)ayI;=z|;M|5FdQp9K5O1qd~gJ4gnPOIHZg#51sV6 z;6K=gE>UVKs98$Y%kfFpt56|g8Djc(K25AW-HL7{R(t|LsY;)ADw7h*TEgrgyk zJFT2OPVYXKgi^>9#g0Vi3VrixydMwU_Gyok^bE@i{43-+?H|##-BZ^+z)N?DM$e$w(4Xf3Bo>F27DB01xiin*BH)2^pC?R{L4b1};YG?EO4^94T&2G#wl zBT5)K8)b5`MP7*K4p_HXKYN21?1lc6ne>y7XVP=-3eTd!`0!8^Ps5C7?WK#-H0ywj zmw5m8IKjlV&kJ=LBqa&--RTI)zb?DwJ$P&8q(|nuI*6V!PU)IaWr&?A_w?z2=V$#5 z>XO^^(Bt&+pwrP#e-*Q^G;w}ZmEQ1-WmQwZ3S(!fImAgtawwliY8A{z&_y#w<3^|1 zVtQmBQ6vQR10{@MV{`|*Gik)T=l?u%;#>@2I-%3kgT{| zc3%4Sy|z*%q1v!L4a>h52gi&ba!PZIT)Dm;9k~Cc>!wUBFtHR+qlCrT4;ziZc`r!r4IKGY228wBGIGLE-Z%st3g@L|YtOFoRh-y2sGe~7U2vQBp^HF< z*Rf#_en>UvQ4eFd^ds~I)`UjY4+wM1YrJaUFl=3Kiup600+iiTSu3sNhSGDR8+2=g zD5uc==m-Ek+{b==)+vb0dByh4`4p|EOuSsU+70mr_fX$HNbR2oE@%Un znBe1WZ8|I-|G5CIyP$jhEVR8>T7dJZn#z9dBW`RXwoln1wFxjG!_~%`#4P{3-ad6j z_TsmJwoUAXch2YDIOjdNCiyhf&iuo&ZFvLxo%;Du>JRI|1nPP1SxChl0U(Yu_~$EO zqwap^{#g!^Z}|54d7d7Gy@$^(y<39JT-9uVyoykLnvnHGt6>?;&omoXNW-f5yI?I_ z>ccb3M^+m-&?kSy%G>gA!4kFNl(^;lo9FpCafoC20^u|miY;-A?R3Lv6DQBjt>41; zY(u4@BcgDnDKi^K4U1-s8_D;4;4<-ic$4$Er=<%)w5^RE$fUkkD{U04Ior0sVokHa znQkdN3EbOHtho#z*su*2@2CU?*vdZcn0uLFCs?($cx+p)4Sh(0sd;lJozye>n2UZ? zEitf)#$OJ$b&KJAwz2#Ucrdk#Pkcl zGbM49cgZawcO4TV5Gh^?#D4jQ5oroRLR_K>OAjTB`~|kydX?LkVL5QEp4_^Km|K=w z0y)8%+SYjy32TTSuH6c&E66gXsrrw&6M`yBUw!qxZtn3C*7V9@KO3E(aOS7_@ktH= z7`$`U`stz7*skF&Khxi(Tao$lqHH%`Lq~&OKQgN|{4Z{5)x?3@Dp|Vd`i7aKriaqc zv!+VfhYj=HQ+bAT!zF}u^Ocpzs~a6kPk_-zk@^l9b+UB$`+tF`_q4K#7fV<@AR8E~ zphS?FC)aFfCwo{{8{LbKXXC-?Y@E!coq@OBbx#Gj`Han|dK2&Q95Mxz{6e&N2bG~3 z%pV=OATeaoV~SVfYsOm6z=NI!-bu=4NcO+_TQ+v@99O05rBHrISfq7iQTQ~dFs#_v ze}Sue%oqP!d>bqRqbj+p+@uNt5}=Mb<#Jwn(7%p5nCWpTcP#4CYj}^RP1094=Ky^1 zX5np>@dA&wb5IfTO7_;WHT{ot0j$7larQ31X2lbpKc|SxEA&;GX$DwS*sIhhgsr$) zH~fv#6RX^PWzi1!YOMR4vwP2Gbaf~x1a6Mq@uHMdtE=m%*FuszalP|mJ|<@@DUzA$ z&t>dhL-RB2;kN;r=jwkXbnN}8d)C30OUUiKV2-;na=c=}W4=y`V>ta8VUl!J%+Ghp z(JW+y#=80asDllw3FHayN}j!uUn~?ThqZv_fJC`VvqaxH*|SBga=d?r)76%KA2L?Z zb=CE*Ofqd${AL<5rYKNMiMnrUm7KC1lJ}3GQ*>TAe6|^wqGK?=`i<;TDMVxg)vHh7Yec zzSJXV?knXaaGechW9?S<=C2TFc_n#G<83^9^!c~%17}Nk-9R@kzQJqmbMF7@=4)A# zff7C={toB{puyi&5zCFcf2ZxOcLKtKe%n7yb3xPD!EK{qrUcFE)wtfYq#QapI!MX? zbNSwzvlsi=i5uPZQ|5P7xIfvVcpIUR{=+s|m?!(9Go>ua>glVv4a0fzT+H9n4KDRt z7yTG&b241+Jn6pMEy2n?25;`C4nNU2CFgmuv_#>YXk=J+x=EBuugSMu{nxg*eV30$ zz|LF5R!Lem*mH*5s802$dlL6j;fl-6!Z@gm4`HB#^0R}2=sV+3R*!Je$Mv~)S>>R` z$MRikX4CVPhO6Y;MOdo(E*1LBR-V{jj*!lj&3|q% za%H^SPg`j`8!f(~-J9<>P@O?Z|8`Yj*`^nHk#3(1&v2wxqFig1r7sIFz5l~-F+VPC z?z$;N4HCaPQq~NAFo$YQ`=FAti1|iB1oEkUEJxny_53!=;SGgBI*gdC773?^KxPm&NvgfIk6|*dOSNn$0 zIN4iafw}7#@-#i2r02QnFHGHfN-{7Gbl4$d>n(v{7;Gvr!{UQM3v9j`XrFkf%yk}!1yr^PW#u}5c_JLD$&YuZ%jmN*h-0C0LGpv^e>)!^i!etEldHkpJFCFG`NiciiP6&eCN5Ri#Vv?s^r#0MqXSF23?I~e+n*QwU}>=*Z$ zbGMBf{et9 zrr6%09q2A|h*6qz0QI|443q|DXVd3i956UD>}C}^i||VHSPV2U3VIde=nGYfH;7!I z0pl=379)MqU2o68{3`3JEI*vc#N#sIe(iIlY0;sU2^_U=>V14L?@Ll_Gt)r?xlg1>3E&n*8kkaZg zmozK!`<3VG3suoC|0?A^Cx#=WfBBgkNM}%|iDfs%J7&USMY4W61}qe1;?pzXN>lw- z{&5hyj@`j2QGTh=>BesiF+Mdo4^v8&Hf4Bbp*Ev6z*qp8l0&Xu1gZ;$$}dg?Im7~w zsm5O}>}$T*E%{6G=R-;26U4>bCkXM@CkWAqCz#8$Cn%lj%_e2z&Wj&UbWQ$T{YkTH zaWT|s{{`|S7y`CSgnX;`I7gLdtKSGC_e$TruhX8gE3Tb&iIQcpN8RD*7ne^2G3NuA f#6Y9}=l>yrB-!&Z{6c1Cj9S_n`mmxK)=&QjJP%hj literal 0 HcmV?d00001 diff --git a/app/services/oauth2/ClientService.php b/app/services/oauth2/ClientService.php index 93cad85d..f44f83ed 100644 --- a/app/services/oauth2/ClientService.php +++ b/app/services/oauth2/ClientService.php @@ -4,6 +4,8 @@ namespace services\oauth2; use Client; use ClientAuthorizedUri; +use ClientAllowedOrigin; + use DB; use Input; use oauth2\exceptions\AllowedClientUriAlreadyExistsException; @@ -20,7 +22,7 @@ use oauth2\services\id; use oauth2\services\OAuth2ServiceCatalog; use Request; use utils\services\IAuthService; -use utils\services\Registry; +use utils\services\ServiceLocator; use Zend\Math\Rand; /** @@ -77,10 +79,10 @@ class ClientService implements IClientService return array($client_id, $client_secret); } - public function addClient($application_type, $user_id, $app_name, $app_description, $app_logo = '') + public function addClient($application_type, $user_id, $app_name, $app_description,$app_url=null, $app_logo = '') { $instance = null; - DB::transaction(function () use ($application_type, $user_id, $app_name, $app_description, $app_logo, &$instance) { + DB::transaction(function () use ($application_type, $user_id, $app_name,$app_url, $app_description, $app_logo, &$instance) { //check $application_type vs client_type $client_type = $application_type == IClient::ApplicationType_JS_Client?IClient::ClientType_Public:IClient::ClientType_Confidential; @@ -98,6 +100,7 @@ class ClientService implements IClientService $instance->active = true; $instance->use_refresh_token = false; $instance->rotate_refresh_token = false; + $instance->website = $app_url; $instance->Save(); //default allowed url $this->addClientAllowedUri($instance->getId(), 'https://localhost'); @@ -115,20 +118,24 @@ class ClientService implements IClientService public function addClientAllowedUri($id, $uri) { - $client = Client::find($id); + $res = false; + DB::transaction(function () use ($id,$uri,&$res){ + $client = Client::find($id); - if (is_null($client)) - throw new AbsentClientException(sprintf("client id %s does not exists!",$id)); + if (is_null($client)) + throw new AbsentClientException(sprintf("client id %s does not exists!",$id)); - $client_uri = ClientAuthorizedUri::where('uri', '=', $uri)->where('client_id', '=', $id)->first(); - if (!is_null($client_uri)) { - throw new AllowedClientUriAlreadyExistsException(sprintf('uri : %s', $uri)); - } + $client_uri = ClientAuthorizedUri::where('uri', '=', $uri)->where('client_id', '=', $id)->first(); + if (!is_null($client_uri)) { + throw new AllowedClientUriAlreadyExistsException(sprintf('uri : %s', $uri)); + } - $client_authorized_uri = new ClientAuthorizedUri; - $client_authorized_uri->client_id = $id; - $client_authorized_uri->uri = $uri; - return $client_authorized_uri->Save(); + $client_authorized_uri = new ClientAuthorizedUri; + $client_authorized_uri->client_id = $id; + $client_authorized_uri->uri = $uri; + $res = $client_authorized_uri->Save(); + }); + return $res; } public function addClientScope($id, $scope_id) @@ -165,7 +172,7 @@ class ClientService implements IClientService if (!is_null($client)) { $client->authorized_uris()->delete(); $client->scopes()->detach(); - $token_service = Registry::getInstance()->get(OAuth2ServiceCatalog::TokenService); + $token_service = ServiceLocator::getInstance()->getService(OAuth2ServiceCatalog::TokenService); $token_service->revokeClientRelatedTokens($client->client_id); $res = $client->delete(); } @@ -193,7 +200,7 @@ class ClientService implements IClientService $client_secret = Rand::getString(24, OAuth2Protocol::VsChar, true); $client->client_secret = $client_secret; $client->Save(); - $token_service = Registry::getInstance()->get(OAuth2ServiceCatalog::TokenService); + $token_service = ServiceLocator::getInstance()->getService(OAuth2ServiceCatalog::TokenService); $token_service->revokeClientRelatedTokens($client->client_id); $new_secret = $client->client_secret; @@ -208,11 +215,15 @@ class ClientService implements IClientService */ public function lockClient($client_id) { - $client = $this->getClientByIdentifier($client_id); - if (is_null($client)) - throw new AbsentClientException($client_id,sprintf("client id %s does not exists!",$client_id)); - $client->locked = true; - return $client->Save(); + $res = false; + DB::transaction(function () use ($client_id, &$res) { + $client = $this->getClientByIdentifier($client_id); + if (is_null($client)) + throw new AbsentClientException($client_id,sprintf("client id %s does not exists!",$client_id)); + $client->locked = true; + $res = $client->Save(); + }); + return $res; } /** @@ -222,11 +233,15 @@ class ClientService implements IClientService */ public function unlockClient($client_id) { - $client = $this->getClientByIdentifier($client_id); - if (is_null($client)) - throw new AbsentClientException($client_id,sprintf("client id %s does not exists!",$client_id)); - $client->locked = false; - return $client->Save(); + $res = false; + DB::transaction(function () use ($client_id, &$res) { + $client = $this->getClientByIdentifier($client_id); + if (is_null($client)) + throw new AbsentClientException($client_id,sprintf("client id %s does not exists!",$client_id)); + $client->locked = false; + $res = $client->Save(); + }); + return $res; } @@ -322,17 +337,65 @@ class ClientService implements IClientService */ public function update($id, array $params) { - $client = Client::find($id); - if(is_null($client)) - throw new AbsentClientException(sprintf('client id %s does not exists!',$id)); + $res = false; + DB::transaction(function () use ($id,$params, &$res) { + $client = Client::find($id); + if(is_null($client)) + throw new AbsentClientException(sprintf('client id %s does not exists!',$id)); - $allowed_update_params = array('app_name','app_description','app_logo','active','locked','use_refresh_token','rotate_refresh_token'); + $allowed_update_params = array( + 'app_name','website','app_description','app_logo','active','locked','use_refresh_token','rotate_refresh_token'); - foreach($allowed_update_params as $param){ - if(array_key_exists($param,$params)){ - $client->{$param} = $params[$param]; + foreach($allowed_update_params as $param){ + if(array_key_exists($param,$params)){ + $client->{$param} = $params[$param]; + } } - } - return $this->save($client); + $res = $this->save($client); + }); + return $res; + } + + /** + * @param $id + * @param $origin + * @return mixed + * @throws \oauth2\exceptions\AllowedClientUriAlreadyExistsException + * @throws \oauth2\exceptions\AbsentClientException + */ + public function addClientAllowedOrigin($id, $origin) + { + $res = false; + DB::transaction(function () use ($id, $origin, &$res) { + $client = Client::find($id); + + if (is_null($client)) + throw new AbsentClientException(sprintf("client id %s does not exists!",$id)); + + if($client->getApplicationType()!=IClient::ApplicationType_JS_Client) + throw new InvalidClientType($id,sprintf("client id %s application type must be JS_CLIENT",$id)); + + $client_origin = ClientAllowedOrigin::where('allowed_origin', '=', $origin)->where('client_id', '=', $id)->first(); + if (!is_null($client_origin)) { + throw new AllowedClientUriAlreadyExistsException(sprintf('origin : %s', $origin)); + } + + $client_origin = new ClientAllowedOrigin; + $client_origin->client_id = $id; + $client_origin->allowed_origin = $origin; + + $res = $client_origin->Save(); + }); + return $res; + } + + /** + * @param $id + * @param $origin_id + * @return mixed + */ + public function deleteClientAllowedOrigin($id, $origin_id) + { + return ClientAllowedOrigin::where('id', '=', $origin_id)->where('client_id', '=', $id)->delete(); } } \ No newline at end of file diff --git a/app/services/oauth2/OAuth2ServiceProvider.php b/app/services/oauth2/OAuth2ServiceProvider.php new file mode 100644 index 00000000..dceaa99f --- /dev/null +++ b/app/services/oauth2/OAuth2ServiceProvider.php @@ -0,0 +1,41 @@ +app->singleton('oauth2\\IResourceServerContext', 'services\\oauth2\\ResourceServerContext'); + + $this->app->singleton(OAuth2ServiceCatalog::MementoService, 'services\\oauth2\\MementoOAuth2AuthenticationRequestService'); + $this->app->singleton(OAuth2ServiceCatalog::ClientService, 'services\\oauth2\\ClientService'); + $this->app->singleton(OAuth2ServiceCatalog::TokenService, 'services\\oauth2\\TokenService'); + $this->app->singleton(OAuth2ServiceCatalog::ScopeService, 'services\\oauth2\\ApiScopeService'); + $this->app->singleton(OAuth2ServiceCatalog::ResourceServerService, 'services\\oauth2\\ResourceServerService'); + $this->app->singleton(OAuth2ServiceCatalog::ApiService, 'services\\oauth2\\ApiService'); + $this->app->singleton(OAuth2ServiceCatalog::ApiEndpointService, 'services\\oauth2\\ApiEndpointService'); + $this->app->singleton(OAuth2ServiceCatalog::UserConsentService, 'services\\oauth2\\UserConsentService'); + $this->app->singleton(OAuth2ServiceCatalog::AllowedOriginService, 'services\\oauth2\\AllowedOriginService'); + //OAUTH2 resource server endpoints + $this->app->singleton('oauth2\resource_server\IUserService', 'services\oauth2\resource_server\UserService'); + } + + public function provides() + { + return array('oauth2.services'); + } +} \ No newline at end of file diff --git a/app/services/oauth2/ResourceServerContext.php b/app/services/oauth2/ResourceServerContext.php index 656daf3d..84bc537c 100644 --- a/app/services/oauth2/ResourceServerContext.php +++ b/app/services/oauth2/ResourceServerContext.php @@ -13,11 +13,11 @@ class ResourceServerContext implements IResourceServerContext { private $auth_context; /** - * @return null|string + * @return array */ public function getCurrentScope() { - return isset($this->auth_context['scope'])?$this->auth_context['scope']:null; + return isset($this->auth_context['scope'])? explode(' ',$this->auth_context['scope']):array(); } /** @@ -45,6 +45,14 @@ class ResourceServerContext implements IResourceServerContext { return isset($this->auth_context['client_id'])?$this->auth_context['client_id']:null; } + /** + * @return null|int + */ + public function getCurrentUserId() + { + return isset($this->auth_context['user_id'])?intval($this->auth_context['user_id']):null; + } + /** * @param $auth_context */ @@ -52,12 +60,4 @@ class ResourceServerContext implements IResourceServerContext { { $this->auth_context = $auth_context; } - - /** - * @return null - */ - public function getCurrentUserId() - { - return isset($this->auth_context['user_id'])?$this->auth_context['user_id']:null; - } } \ No newline at end of file diff --git a/app/services/oauth2/RevokeAuthorizationCodeRelatedTokens.php b/app/services/oauth2/RevokeAuthorizationCodeRelatedTokens.php index b17da32a..f02e1222 100644 --- a/app/services/oauth2/RevokeAuthorizationCodeRelatedTokens.php +++ b/app/services/oauth2/RevokeAuthorizationCodeRelatedTokens.php @@ -5,7 +5,7 @@ namespace services\oauth2; use Exception; use Log; use oauth2\services\OAuth2ServiceCatalog; -use utils\services\Registry; +use utils\services\ServiceLocator; use utils\services\ISecurityPolicyCounterMeasure; @@ -32,8 +32,8 @@ class RevokeAuthorizationCodeRelatedTokens implements ISecurityPolicyCounterMeas $auth_code = $params["auth_code"]; //$client_id = $params["client_id"]; - $token_service = Registry::getInstance()->get(OAuth2ServiceCatalog::TokenService); - //$client_service = Registry::getInstance()->get(OAuth2ServiceCatalog::ClientService); + $token_service = ServiceLocator::getInstance()->getService(OAuth2ServiceCatalog::TokenService); + //$client_service = ServiceLocator::getInstance()->getService(OAuth2ServiceCatalog::ClientService); $token_service->revokeAuthCodeRelatedTokens($auth_code); diff --git a/app/services/oauth2/TokenService.php b/app/services/oauth2/TokenService.php index 4fca09e8..206cd58c 100644 --- a/app/services/oauth2/TokenService.php +++ b/app/services/oauth2/TokenService.php @@ -23,7 +23,7 @@ use oauth2\services\IUserConsentService; use RefreshToken as RefreshTokenDB; use RefreshToken as DBRefreshToken; -use services\IPHelper; +use utils\IPHelper; use utils\exceptions\UnacquiredLockException; use utils\services\ILockManagerService; diff --git a/app/services/oauth2/resource_server/UserService.php b/app/services/oauth2/resource_server/UserService.php new file mode 100644 index 00000000..a566b7fb --- /dev/null +++ b/app/services/oauth2/resource_server/UserService.php @@ -0,0 +1,73 @@ +user_service = $user_service; + } + + /** + * Get Current user info + * @return array + * @throws Exception + */ + public function getCurrentUserInfo() + { + $data = array(); + try{ + + $me = $this->resource_server_context->getCurrentUserId(); + + if(is_null($me)){ + throw new Exception('me is no set!.'); + } + + $current_user = $this->user_service->get($me); + $scopes = $this->resource_server_context->getCurrentScope(); + + if(in_array(self::UserProfileScope_Address, $scopes)){ + // Address Claim + $data['country'] = $current_user->getCountry(); + $data['street_address'] = $current_user->getCountry(); + $data['postal_code'] = $current_user->getPostalCode(); + $data['region'] = $current_user->getRegion(); + $data['locality'] = $current_user->getLocality(); + } + if(in_array(self::UserProfileScope_Profile, $scopes)){ + // Address Claim + $data['name'] = $current_user->getFirstName(); + $data['family_name'] = $current_user->getLastName(); + $data['nickname'] = $current_user->getNickName(); + $data['picture'] = $current_user->getPic(); + $data['birthdate'] = $current_user->getDateOfBirth(); + $data['gender'] = $current_user->getGender(); + } + if(in_array(self::UserProfileScope_Email, $scopes)){ + // Address Claim + $data['email'] = $current_user->getEmail(); + } + } + catch(Exception $ex){ + $this->log_service->error($ex); + throw $ex; + } + return $data; + } +} \ No newline at end of file diff --git a/app/services/openid/AssociationService.php b/app/services/openid/AssociationService.php index 3fcef364..6cbb959a 100644 --- a/app/services/openid/AssociationService.php +++ b/app/services/openid/AssociationService.php @@ -1,6 +1,6 @@ lock_manager_service = $lock_manager_service; - $this->cache_service = $cache_service; + $this->lock_manager_service = $lock_manager_service; + $this->cache_service = $cache_service; + $this->configuration_service = $configuration_service; } /** @@ -33,7 +38,7 @@ class NonceService implements INonceService public function lockNonce(OpenIdNonce $nonce) { $raw_nonce = $nonce->getRawFormat(); - $lock_lifetime = \ServerConfigurationService::getConfigValue("Nonce.Lifetime"); + $lock_lifetime = $this->configuration_service->getConfigValue("Nonce.Lifetime"); try { $this->lock_manager_service->acquireLock('lock.nonce.' . $raw_nonce, $lock_lifetime); } catch (UnacquiredLockException $ex) { @@ -91,7 +96,7 @@ class NonceService implements INonceService { try { $raw_nonce = $nonce->getRawFormat(); - $lifetime = \ServerConfigurationService::getConfigValue("Nonce.Lifetime"); + $lifetime = $this->configuration_service->getConfigValue("Nonce.Lifetime"); $this->cache_service->setSingleValue($raw_nonce . $signature, $realm, $lifetime ); } catch (Exception $ex) { Log::error($ex); diff --git a/app/services/openid/OpenIdProvider.php b/app/services/openid/OpenIdProvider.php new file mode 100644 index 00000000..8642d338 --- /dev/null +++ b/app/services/openid/OpenIdProvider.php @@ -0,0 +1,33 @@ +app->singleton(OpenIdServiceCatalog::MementoService, 'services\\openid\\MementoRequestService'); + $this->app->singleton(OpenIdServiceCatalog::AuthenticationStrategy, 'services\\openid\\AuthenticationStrategy'); + $this->app->singleton(OpenIdServiceCatalog::ServerExtensionsService, 'services\\openid\\ServerExtensionsService'); + $this->app->singleton(OpenIdServiceCatalog::AssociationService, 'services\\openid\\AssociationService'); + $this->app->singleton(OpenIdServiceCatalog::TrustedSitesService, 'services\\openid\\TrustedSitesService'); + $this->app->singleton(OpenIdServiceCatalog::ServerConfigurationService, 'services\\utils\\ServerConfigurationService'); + $this->app->singleton(OpenIdServiceCatalog::UserService, 'services\\openid\\UserService'); + $this->app->singleton(OpenIdServiceCatalog::NonceService, 'services\\openid\\NonceService'); + } + + public function provides() + { + return array('openid.services'); + } +} \ No newline at end of file diff --git a/app/services/openid/ServerExtensionsService.php b/app/services/openid/ServerExtensionsService.php index 9ce44df9..b39e3054 100644 --- a/app/services/openid/ServerExtensionsService.php +++ b/app/services/openid/ServerExtensionsService.php @@ -1,20 +1,27 @@ get(); + $extensions = ServerExtension::where('active', '=', true)->get(); $res = array(); foreach ($extensions as $extension) { $class = $extension->extension_class; if (empty($class) /*|| !class_exists($class)*/) continue; - $implementation = new $class($extension->name, $extension->namespace, $extension->view_name, $extension->description); + $implementation = new $class($extension->name, + $extension->namespace, + $extension->view_name, + $extension->description, + ServiceLocator::getInstance()->getService(UtilsServiceCatalog::LogService)); array_push($res, $implementation); } return $res; diff --git a/app/services/openid/TrustedSitesService.php b/app/services/openid/TrustedSitesService.php index d3aebcbf..6df9035e 100644 --- a/app/services/openid/TrustedSitesService.php +++ b/app/services/openid/TrustedSitesService.php @@ -1,6 +1,6 @@ get(OpenIdServiceCatalog::ServerConfigurationService); - $user_service = Registry::getInstance()->get(OpenIdServiceCatalog::UserService); + $server_configuration = ServiceLocator::getInstance()->getService(OpenIdServiceCatalog::ServerConfigurationService); + $user_service = ServiceLocator::getInstance()->getService(OpenIdServiceCatalog::UserService); $user = User::where('external_id', '=', $user_identifier)->first(); if(is_null($user)) return; //apply lock policy - if ($user->login_failed_attempt < $server_configuration->getConfigValue("MaxFailed.Login.Attempts")) + if (intval($user->login_failed_attempt) < intval($server_configuration->getConfigValue("MaxFailed.Login.Attempts"))) $user_service->updateFailedLoginAttempts($user->id); else { $user_service->lockUser($user->id); diff --git a/app/services/security_policies/OAuth2LockClientCounterMeasure.php b/app/services/security_policies/OAuth2LockClientCounterMeasure.php index 3a286c46..b3628613 100644 --- a/app/services/security_policies/OAuth2LockClientCounterMeasure.php +++ b/app/services/security_policies/OAuth2LockClientCounterMeasure.php @@ -5,7 +5,7 @@ namespace services; use Exception; use Log; use oauth2\services\OAuth2ServiceCatalog; -use utils\services\Registry; +use utils\services\ServiceLocator; use utils\services\ISecurityPolicyCounterMeasure; use Client as OAuth2Client; @@ -18,7 +18,7 @@ class OAuth2LockClientCounterMeasure implements ISecurityPolicyCounterMeasure{ if (!isset($params["client_id"])) return; $client_id = $params['client_id']; - $client_service = Registry::getInstance()->get(OAuth2ServiceCatalog::ClientService); + $client_service = ServiceLocator::getInstance()->getService(OAuth2ServiceCatalog::ClientService); $client = OAuth2Client::where('id', '=', client_id)->first(); if(is_null($client)) return; diff --git a/app/services/security_policies/OAuth2SecurityPolicy.php b/app/services/security_policies/OAuth2SecurityPolicy.php index 044de4db..913cf805 100644 --- a/app/services/security_policies/OAuth2SecurityPolicy.php +++ b/app/services/security_policies/OAuth2SecurityPolicy.php @@ -5,13 +5,13 @@ namespace services; use DB; use Exception; use Log; -use oauth2\services\IClientService; use oauth2\services\OAuth2ServiceCatalog; use utils\services\ISecurityPolicy; use utils\services\ISecurityPolicyCounterMeasure; use OAuth2TrailException; use utils\services\IServerConfigurationService; -use utils\services\Registry; +use utils\services\ServiceLocator; +use utils\IPHelper; /** * Class OAuth2SecurityPolicy @@ -30,8 +30,9 @@ class OAuth2SecurityPolicy implements ISecurityPolicy{ ; $this->exception_dictionary = array( 'auth2\exceptions\BearerTokenDisclosureAttemptException' => array('OAuth2SecurityPolicy.MaxBearerTokenDisclosureAttempts'), - 'auth2\exceptions\InvalidClientException' => array('OAuth2SecurityPolicy.MaxInvalidClientExceptionAttempts'), - 'auth2\exceptions\InvalidRedeemAuthCodeException' => array('OAuth2SecurityPolicy.MaxInvalidRedeemAuthCodeAttempts'), + 'auth2\exceptions\InvalidClientException' => array('OAuth2SecurityPolicy.MaxInvalidClientExceptionAttempts'), + 'auth2\exceptions\InvalidRedeemAuthCodeException' => array('OAuth2SecurityPolicy.MaxInvalidRedeemAuthCodeAttempts'), + 'auth2\exceptions\InvalidClientCredentials' => array('OAuth2SecurityPolicy.MaxInvalidInvalidClientCredentialsAttempts'), ); } /** @@ -52,7 +53,7 @@ class OAuth2SecurityPolicy implements ISecurityPolicy{ { try { if(get_parent_class($ex)=='oauth2\\exceptions\\OAuth2ClientBaseException'){ - $this->client_service = Registry::getInstance()->get(OAuth2ServiceCatalog::ClientService); + $this->client_service = ServiceLocator::getInstance()->getService(OAuth2ServiceCatalog::ClientService); $client_id = $ex->getClientId(); //save oauth2 exception by client id if (!is_null($client_id) && !empty($client_id)){ diff --git a/app/services/CheckPointService.php b/app/services/utils/CheckPointService.php similarity index 92% rename from app/services/CheckPointService.php rename to app/services/utils/CheckPointService.php index 8bad948a..48a96c25 100644 --- a/app/services/CheckPointService.php +++ b/app/services/utils/CheckPointService.php @@ -1,12 +1,14 @@ from_ip = $remote_ip; $user_trail->exception_type = $class_name; if(Auth::check()){ diff --git a/app/services/security_policies/LockManagerService.php b/app/services/utils/LockManagerService.php similarity index 96% rename from app/services/security_policies/LockManagerService.php rename to app/services/utils/LockManagerService.php index f926a03c..d88cf29f 100644 --- a/app/services/security_policies/LockManagerService.php +++ b/app/services/utils/LockManagerService.php @@ -1,6 +1,6 @@ redis = \RedisLV4::connection(); + } + + public function boot(){ + if(is_null($this->redis)){ + $this->redis = \RedisLV4::connection(); + } + } /** * @param $key * @return mixed diff --git a/app/services/ServerConfigurationService.php b/app/services/utils/ServerConfigurationService.php similarity index 54% rename from app/services/ServerConfigurationService.php rename to app/services/utils/ServerConfigurationService.php index 1f11a522..7d66489f 100644 --- a/app/services/ServerConfigurationService.php +++ b/app/services/utils/ServerConfigurationService.php @@ -1,13 +1,15 @@ cache_service = $cache_service; + $this->cache_service = $cache_service; $this->default_config_params = array(); //default config values //general - $this->default_config_params["MaxFailed.Login.Attempts"] = 10; - $this->default_config_params["MaxFailed.LoginAttempts.2ShowCaptcha"] = 3; - $this->default_config_params["Assets.Url"] = 'http://www.openstack.org/'; + $this->default_config_params["MaxFailed.Login.Attempts"] = Config::get('server.MaxFailed_Login_Attempts', 10); + $this->default_config_params["MaxFailed.LoginAttempts.2ShowCaptcha"] = Config::get('server.MaxFailed_LoginAttempts_2ShowCaptcha', 3); + $this->default_config_params["Assets.Url"] = Config::get('server.Assets_Url', 'http://www.openstack.org/'); //openid - $this->default_config_params["OpenId.Private.Association.Lifetime"] = 240; - $this->default_config_params["OpenId.Session.Association.Lifetime"] = 21600; - $this->default_config_params["OpenId.Nonce.Lifetime"] = 360; + $this->default_config_params["OpenId.Private.Association.Lifetime"] = Config::get('server.OpenId_Private_Association_Lifetime', 240); + $this->default_config_params["OpenId.Session.Association.Lifetime"] = Config::get('server.OpenId_Session_Association_Lifetime', 21600); + $this->default_config_params["OpenId.Nonce.Lifetime"] = Config::get('server.OpenId_Nonce_Lifetime', 360); //policies - $this->default_config_params["BlacklistSecurityPolicy.BannedIpLifeTimeSeconds"] = 21600; - $this->default_config_params["BlacklistSecurityPolicy.MinutesWithoutExceptions"] = 5; - $this->default_config_params["BlacklistSecurityPolicy.ReplayAttackExceptionInitialDelay"] = 10; - $this->default_config_params["BlacklistSecurityPolicy.MaxInvalidNonceAttempts"] = 10; - $this->default_config_params["BlacklistSecurityPolicy.InvalidNonceInitialDelay"] = 10; - $this->default_config_params["BlacklistSecurityPolicy.MaxInvalidOpenIdMessageExceptionAttempts"] = 10; - $this->default_config_params["BlacklistSecurityPolicy.InvalidOpenIdMessageExceptionInitialDelay"] = 10; - $this->default_config_params["BlacklistSecurityPolicy.MaxOpenIdInvalidRealmExceptionAttempts"] = 10; - $this->default_config_params["BlacklistSecurityPolicy.OpenIdInvalidRealmExceptionInitialDelay"] = 10; - $this->default_config_params["BlacklistSecurityPolicy.MaxInvalidOpenIdMessageModeAttempts"] = 10; - $this->default_config_params["BlacklistSecurityPolicy.InvalidOpenIdMessageModeInitialDelay"] = 10; - $this->default_config_params["BlacklistSecurityPolicy.MaxInvalidOpenIdAuthenticationRequestModeAttempts"] = 10; - $this->default_config_params["BlacklistSecurityPolicy.InvalidOpenIdAuthenticationRequestModeInitialDelay"] = 10; - $this->default_config_params["BlacklistSecurityPolicy.MaxAuthenticationExceptionAttempts"] = 10; - $this->default_config_params["BlacklistSecurityPolicy.AuthenticationExceptionInitialDelay"] = 20; - $this->default_config_params["BlacklistSecurityPolicy.MaxInvalidAssociationAttempts"] = 10; - $this->default_config_params["BlacklistSecurityPolicy.InvalidAssociationInitialDelay"] = 20; + $this->default_config_params["BlacklistSecurityPolicy.BannedIpLifeTimeSeconds"] = Config::get('server.BlacklistSecurityPolicy_BannedIpLifeTimeSeconds', 21600); + $this->default_config_params["BlacklistSecurityPolicy.MinutesWithoutExceptions"] = Config::get('server.BlacklistSecurityPolicy_MinutesWithoutExceptions', 5);; + $this->default_config_params["BlacklistSecurityPolicy.ReplayAttackExceptionInitialDelay"] = Config::get('server.BlacklistSecurityPolicy_ReplayAttackExceptionInitialDelay', 10); + $this->default_config_params["BlacklistSecurityPolicy.MaxInvalidNonceAttempts"] = Config::get('server.BlacklistSecurityPolicy_MaxInvalidNonceAttempts', 10); + $this->default_config_params["BlacklistSecurityPolicy.InvalidNonceInitialDelay"] = Config::get('server.BlacklistSecurityPolicy_InvalidNonceInitialDelay', 10); + $this->default_config_params["BlacklistSecurityPolicy.MaxInvalidOpenIdMessageExceptionAttempts"] = Config::get('server.BlacklistSecurityPolicy_MaxInvalidOpenIdMessageExceptionAttempts', 10); + $this->default_config_params["BlacklistSecurityPolicy.InvalidOpenIdMessageExceptionInitialDelay"] = Config::get('server.BlacklistSecurityPolicy_InvalidOpenIdMessageExceptionInitialDelay', 10); + $this->default_config_params["BlacklistSecurityPolicy.MaxOpenIdInvalidRealmExceptionAttempts"] = Config::get('server.BlacklistSecurityPolicy_MaxOpenIdInvalidRealmExceptionAttempts', 10); + $this->default_config_params["BlacklistSecurityPolicy.OpenIdInvalidRealmExceptionInitialDelay"] = Config::get('server.BlacklistSecurityPolicy_OpenIdInvalidRealmExceptionInitialDelay', 10); + $this->default_config_params["BlacklistSecurityPolicy.MaxInvalidOpenIdMessageModeAttempts"] = Config::get('server.BlacklistSecurityPolicy_MaxInvalidOpenIdMessageModeAttempts', 10); + $this->default_config_params["BlacklistSecurityPolicy.InvalidOpenIdMessageModeInitialDelay"] = Config::get('server.BlacklistSecurityPolicy_InvalidOpenIdMessageModeInitialDelay', 10); + $this->default_config_params["BlacklistSecurityPolicy.MaxInvalidOpenIdAuthenticationRequestModeAttempts"] = Config::get('server.BlacklistSecurityPolicy_MaxInvalidOpenIdAuthenticationRequestModeAttempts', 10); + $this->default_config_params["BlacklistSecurityPolicy.InvalidOpenIdAuthenticationRequestModeInitialDelay"] = Config::get('server.BlacklistSecurityPolicy_InvalidOpenIdAuthenticationRequestModeInitialDelay', 10); + $this->default_config_params["BlacklistSecurityPolicy.MaxAuthenticationExceptionAttempts"] = Config::get('server.BlacklistSecurityPolicy_MaxAuthenticationExceptionAttempts', 10); + $this->default_config_params["BlacklistSecurityPolicy.AuthenticationExceptionInitialDelay"] = Config::get('server.BlacklistSecurityPolicy_AuthenticationExceptionInitialDelay', 20); + $this->default_config_params["BlacklistSecurityPolicy.MaxInvalidAssociationAttempts"] = Config::get('server.BlacklistSecurityPolicy_MaxInvalidAssociationAttempts', 10); + $this->default_config_params["BlacklistSecurityPolicy.InvalidAssociationInitialDelay"] = Config::get('server.BlacklistSecurityPolicy_InvalidAssociationInitialDelay', 20); //oauth2 - $this->default_config_params["BlacklistSecurityPolicy.OAuth2.MaxAuthCodeReplayAttackAttempts"] = 3; - $this->default_config_params["BlacklistSecurityPolicy.OAuth2.AuthCodeReplayAttackInitialDelay"] = 10; + $this->default_config_params["BlacklistSecurityPolicy.OAuth2.MaxAuthCodeReplayAttackAttempts"] = Config::get('server.BlacklistSecurityPolicy_OAuth2_MaxAuthCodeReplayAttackAttempts', 3); + $this->default_config_params["BlacklistSecurityPolicy.OAuth2.AuthCodeReplayAttackInitialDelay"] = Config::get('server.BlacklistSecurityPolicy_OAuth2_AuthCodeReplayAttackInitialDelay', 10); - $this->default_config_params["BlacklistSecurityPolicy.OAuth2.MaxInvalidAuthorizationCodeAttempts"] = 3; - $this->default_config_params["BlacklistSecurityPolicy.OAuth2.InvalidAuthorizationCodeInitialDelay"] = 10; + $this->default_config_params["BlacklistSecurityPolicy.OAuth2.MaxInvalidAuthorizationCodeAttempts"] = Config::get('server.BlacklistSecurityPolicy_OAuth2_MaxInvalidAuthorizationCodeAttempts', 3); + $this->default_config_params["BlacklistSecurityPolicy.OAuth2.InvalidAuthorizationCodeInitialDelay"] = Config::get('server.BlacklistSecurityPolicy_OAuth2_InvalidAuthorizationCodeInitialDelay', 10); - $this->default_config_params["BlacklistSecurityPolicy.OAuth2.MaxInvalidBearerTokenDisclosureAttempt"] = 3; - $this->default_config_params["BlacklistSecurityPolicy.OAuth2.BearerTokenDisclosureAttemptInitialDelay"] = 10; + $this->default_config_params["BlacklistSecurityPolicy.OAuth2.MaxInvalidBearerTokenDisclosureAttempt"] = Config::get('server.BlacklistSecurityPolicy_OAuth2_MaxInvalidBearerTokenDisclosureAttempt', 3); + $this->default_config_params["BlacklistSecurityPolicy.OAuth2.BearerTokenDisclosureAttemptInitialDelay"] = Config::get('server.BlacklistSecurityPolicy_OAuth2_BearerTokenDisclosureAttemptInitialDelay', 10); - $this->default_config_params["OAuth2.AuthorizationCode.Lifetime"] = 600; - $this->default_config_params["OAuth2.AccessToken.Lifetime"] = 3600; + $this->default_config_params["OAuth2.AuthorizationCode.Lifetime"] = Config::get('server.OAuth2_AuthorizationCode_Lifetime', 240); + $this->default_config_params["OAuth2.AccessToken.Lifetime"] = Config::get('server.OAuth2_AccessToken_Lifetime', 3600); //infinite by default - $this->default_config_params["OAuth2.RefreshToken.Lifetime"] = 0; + $this->default_config_params["OAuth2.RefreshToken.Lifetime"] = Config::get('server.OAuth2_RefreshToken_Lifetime', 0); //oauth2 policy defaults - $this->default_config_params["OAuth2SecurityPolicy.MinutesWithoutExceptions"] = 2; - $this->default_config_params["OAuth2SecurityPolicy.MaxBearerTokenDisclosureAttempts"] = 5; - $this->default_config_params["OAuth2SecurityPolicy.MaxInvalidClientExceptionAttempts"] = 10; - $this->default_config_params["OAuth2SecurityPolicy.MaxInvalidRedeemAuthCodeAttempts"] = 10; + $this->default_config_params["OAuth2SecurityPolicy.MinutesWithoutExceptions"] = Config::get('server.OAuth2SecurityPolicy_MinutesWithoutExceptions', 2); + $this->default_config_params["OAuth2SecurityPolicy.MaxBearerTokenDisclosureAttempts"] = Config::get('server.OAuth2SecurityPolicy_MaxBearerTokenDisclosureAttempts', 5); + $this->default_config_params["OAuth2SecurityPolicy.MaxInvalidClientExceptionAttempts"] = Config::get('server.OAuth2SecurityPolicy_MaxInvalidClientExceptionAttempts', 10); + $this->default_config_params["OAuth2SecurityPolicy.MaxInvalidRedeemAuthCodeAttempts"] = Config::get('server.OAuth2SecurityPolicy_MaxInvalidRedeemAuthCodeAttempts', 10); + $this->default_config_params["OAuth2SecurityPolicy.MaxInvalidInvalidClientCredentialsAttempts"] = Config::get('server.OAuth2SecurityPolicy_MaxInvalidInvalidClientCredentialsAttempts', 5); } public function getUserIdentityEndpointURL($identifier) @@ -102,7 +105,7 @@ class ServerConfigurationService implements IOpenIdServerConfigurationService, I return $url; } - /** + /** * get config value from cache and if not in cache check for it on table server_configuration * @param $key * @return mixed @@ -110,7 +113,7 @@ class ServerConfigurationService implements IOpenIdServerConfigurationService, I public function getConfigValue($key) { $res = null; - DB::transaction(function () use ($key,&$res) { + DB::transaction(function () use ($key, &$res) { try { if (!$this->cache_service->exists($key)) { @@ -120,7 +123,7 @@ class ServerConfigurationService implements IOpenIdServerConfigurationService, I else if (isset($this->default_config_params[$key])) $this->cache_service->addSingleValue($key, $this->default_config_params[$key]); - else{ + else { $res = null; return; } @@ -146,15 +149,14 @@ class ServerConfigurationService implements IOpenIdServerConfigurationService, I public function saveConfigValue($key, $value) { $res = false; - DB::transaction(function () use ($key, $value,&$res) { + DB::transaction(function () use ($key, $value, &$res) { $conf = ServerConfiguration::where('key', '=', $key)->first(); - if(is_null($conf)){ + if (is_null($conf)) { $conf = new ServerConfiguration(); $conf->key = $key; $conf->value = $value; - $res=$conf->Save(); - } - else{ + $res = $conf->Save(); + } else { $conf->value = $value; $res = $conf->Save(); } diff --git a/app/services/utils/UtilsProvider.php b/app/services/utils/UtilsProvider.php new file mode 100644 index 00000000..db13b871 --- /dev/null +++ b/app/services/utils/UtilsProvider.php @@ -0,0 +1,50 @@ +app->singleton(UtilsServiceCatalog::CacheService, 'services\\utils\\RedisCacheService'); + + App::resolving('redis',function($redis){ + $cache_service = $this->app->make(UtilsServiceCatalog::CacheService); + $cache_service->boot(); + }); + + $this->app['serverconfigurationservice'] = $this->app->share(function ($app) { + return new ServerConfigurationService($this->app->make(UtilsServiceCatalog::CacheService)); + }); + + // Shortcut so developers don't need to add an Alias in app/config/app.php + $this->app->booting(function () { + $loader = AliasLoader::getInstance(); + $loader->alias('ServerConfigurationService', 'services\\facades\\ServerConfigurationService'); + }); + + $this->app->singleton(UtilsServiceCatalog::LogService, 'services\\utils\\LogService'); + $this->app->singleton(UtilsServiceCatalog::LockManagerService, 'services\\utils\\LockManagerService'); + $this->app->singleton(UtilsServiceCatalog::ServerConfigurationService, 'services\\utils\\ServerConfigurationService'); + $this->app->singleton(UtilsServiceCatalog::BannedIpService, 'services\\utils\\BannedIPService'); + } + + public function provides() + { + return array('utils.services'); + } + + public function when(){ + return array('redis'); + } +} \ No newline at end of file diff --git a/app/start/global.php b/app/start/global.php index 035ba51b..5f369b69 100644 --- a/app/start/global.php +++ b/app/start/global.php @@ -11,7 +11,7 @@ | */ use openid\exceptions\InvalidOpenIdMessageException; -use utils\services\Registry; +use utils\services\ServiceLocator; use utils\services\UtilsServiceCatalog; use oauth2\exceptions\InvalidOAuth2Request; use Monolog\Logger; @@ -91,7 +91,7 @@ if (Config::get('database.log', false)){ App::error(function (Exception $exception, $code) { - $checkpoint_service = Registry::getInstance()->get(UtilsServiceCatalog::CheckPointService); + $checkpoint_service = ServiceLocator::getInstance()->getService(UtilsServiceCatalog::CheckPointService); Log::error($exception); if($checkpoint_service ){ $checkpoint_service->trackException($exception); @@ -101,7 +101,7 @@ App::error(function (Exception $exception, $code) { App::error(function (InvalidOpenIdMessageException $exception, $code) { - $checkpoint_service = Registry::getInstance()->get(UtilsServiceCatalog::CheckPointService); + $checkpoint_service = ServiceLocator::getInstance()->getService(UtilsServiceCatalog::CheckPointService); Log::error($exception); if($checkpoint_service ){ $checkpoint_service->trackException($exception); @@ -110,7 +110,7 @@ App::error(function (InvalidOpenIdMessageException $exception, $code) { }); App::error(function (InvalidOAuth2Request $exception, $code) { - $checkpoint_service = Registry::getInstance()->get(UtilsServiceCatalog::CheckPointService); + $checkpoint_service = ServiceLocator::getInstance()->getService(UtilsServiceCatalog::CheckPointService); Log::error($exception); if($checkpoint_service ){ $checkpoint_service->trackException($exception); diff --git a/app/strategies/DefaultLoginStrategy.php b/app/strategies/DefaultLoginStrategy.php index d82a37c2..30c6598f 100644 --- a/app/strategies/DefaultLoginStrategy.php +++ b/app/strategies/DefaultLoginStrategy.php @@ -3,7 +3,7 @@ namespace strategies; use Auth; use Redirect; -use services\IPHelper; +use utils\IPHelper; use services\IUserActionService; use utils\services\IAuthService; use View; diff --git a/app/strategies/DirectResponseStrategy.php b/app/strategies/DirectResponseStrategy.php index 949129c0..164b2baa 100644 --- a/app/strategies/DirectResponseStrategy.php +++ b/app/strategies/DirectResponseStrategy.php @@ -14,9 +14,6 @@ class DirectResponseStrategy implements IHttpResponseStrategy $http_response->header('Content-Type', $response->getContentType()); $http_response->header('Cache-Control','no-cache, no-store, max-age=0, must-revalidate'); $http_response->header('Pragma','no-cache'); - $http_response->header('X-content-type-options','nosniff'); - $http_response->header('X-xss-protection','1; mode=block'); - $http_response->header('X-frame-options','SAMEORIGIN'); return $http_response; } } \ No newline at end of file diff --git a/app/strategies/OAuth2ConsentStrategy.php b/app/strategies/OAuth2ConsentStrategy.php index 5dfa3b1c..4189db2d 100644 --- a/app/strategies/OAuth2ConsentStrategy.php +++ b/app/strategies/OAuth2ConsentStrategy.php @@ -34,15 +34,16 @@ class OAuth2ConsentStrategy implements IConsentStrategy { public function getConsent() { - $request = $this->memento_service->getCurrentAuthorizationRequest(); - $client_id = $request->getClientId(); - $client = $this->client_service->getClientById($client_id); - $scopes = explode(' ',$request->getScope()); - $requested_scopes = $this->scope_service->getScopesByName($scopes); + $request = $this->memento_service->getCurrentAuthorizationRequest(); + $client_id = $request->getClientId(); + $client = $this->client_service->getClientById($client_id); + $scopes = explode(' ',$request->getScope()); + $requested_scopes = $this->scope_service->getScopesByName($scopes); $data = array(); $data['requested_scopes'] = $requested_scopes; $data['app_name'] = $client->getApplicationName(); $data['redirect_to'] = $request->getRedirectUri(); + $data['website'] = $client->getWebsite(); $app_logo = $client->getApplicationLogo(); diff --git a/app/strategies/OpenIdConsentStrategy.php b/app/strategies/OpenIdConsentStrategy.php index 3c688d32..803f3347 100644 --- a/app/strategies/OpenIdConsentStrategy.php +++ b/app/strategies/OpenIdConsentStrategy.php @@ -9,7 +9,7 @@ use openid\OpenIdProtocol; use openid\services\IMementoOpenIdRequestService; use openid\services\IServerConfigurationService; use Redirect; -use services\IPHelper; +use utils\IPHelper; use services\IUserActionService; use Session; use utils\services\IAuthService; diff --git a/app/strategies/OpenIdLoginStrategy.php b/app/strategies/OpenIdLoginStrategy.php index 87af7d3e..18206816 100644 --- a/app/strategies/OpenIdLoginStrategy.php +++ b/app/strategies/OpenIdLoginStrategy.php @@ -10,7 +10,7 @@ use openid\requests\OpenIdAuthenticationRequest; use openid\responses\OpenIdNonImmediateNegativeAssertion; use openid\services\IMementoOpenIdRequestService; use openid\strategies\OpenIdResponseStrategyFactoryMethod; -use services\IPHelper; +use utils\IPHelper; use services\IUserActionService; use utils\services\IAuthService; diff --git a/app/strategies/StrategyProvider.php b/app/strategies/StrategyProvider.php index 159cb1fe..dba155c5 100644 --- a/app/strategies/StrategyProvider.php +++ b/app/strategies/StrategyProvider.php @@ -7,13 +7,16 @@ use oauth2\responses\OAuth2DirectResponse; use oauth2\responses\OAuth2IndirectResponse; use openid\responses\OpenIdDirectResponse; use openid\responses\OpenIdIndirectResponse; -use utils\services\Registry; use oauth2\responses\OAuth2IndirectFragmentResponse; class StrategyProvider extends ServiceProvider { public function boot() + { + } + + public function register() { //direct response strategy $this->app->singleton(OAuth2DirectResponse::OAuth2DirectResponse, 'strategies\\DirectResponseStrategy'); @@ -22,19 +25,11 @@ class StrategyProvider extends ServiceProvider $this->app->singleton(OpenIdIndirectResponse::OpenIdIndirectResponse, 'strategies\\IndirectResponseQueryStringStrategy'); $this->app->singleton(OAuth2IndirectResponse::OAuth2IndirectResponse, 'strategies\\IndirectResponseQueryStringStrategy'); $this->app->singleton(OAuth2IndirectFragmentResponse::OAuth2IndirectFragmentResponse,'strategies\\IndirectResponseUrlFragmentStrategy'); - $this->app->singleton('oauth2\\strategies\\IOAuth2AuthenticationStrategy', 'strategies\\OAuth2AuthenticationStrategy'); - - Registry::getInstance()->set(OpenIdDirectResponse::OpenIdDirectResponse, $this->app->make(OpenIdDirectResponse::OpenIdDirectResponse)); - Registry::getInstance()->set(OAuth2DirectResponse::OAuth2DirectResponse, $this->app->make(OAuth2DirectResponse::OAuth2DirectResponse)); - - Registry::getInstance()->set(OpenIdIndirectResponse::OpenIdIndirectResponse, $this->app->make(OpenIdIndirectResponse::OpenIdIndirectResponse)); - Registry::getInstance()->set(OAuth2IndirectResponse::OAuth2IndirectResponse, $this->app->make(OAuth2IndirectResponse::OAuth2IndirectResponse)); - Registry::getInstance()->set(OAuth2IndirectFragmentResponse::OAuth2IndirectFragmentResponse, $this->app->make(OAuth2IndirectFragmentResponse::OAuth2IndirectFragmentResponse)); } - public function register() + public function provides() { - + return array('strategies'); } } \ No newline at end of file diff --git a/app/tests/OAuth2UserServiceApiTest.php b/app/tests/OAuth2UserServiceApiTest.php new file mode 100644 index 00000000..85c5b84d --- /dev/null +++ b/app/tests/OAuth2UserServiceApiTest.php @@ -0,0 +1,103 @@ +current_realm = Config::get('app.url'); + + $scope = array( + IUserService::UserProfileScope_Address, + IUserService::UserProfileScope_Email, + IUserService::UserProfileScope_Profile + ); + + $this->client_id = 'Jiz87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x.openstack.client'; + $this->client_secret = 'ITc/6Y5N7kOtGKhg'; + + $params = array( + 'client_id' => $this->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, + ); + + $user = User::where('external_id', '=', 'smarcet@gmail.com')->first(); + + Auth::login($user); + + Session::set("openid.authorization.response", IAuthService::AuthorizationResponse_AllowOnce); + + $response = $this->action("POST", "OAuth2ProviderController@authorize", + $params, + array(), + array(), + array()); + + $status = $response->getStatusCode(); + $url = $response->getTargetUrl(); + $content = $response->getContent(); + + $comps = @parse_url($url); + $query = $comps['query']; + $output = array(); + parse_str($query, $output); + + $params = array( + 'code' => $output['code'], + 'redirect_uri' => 'https://www.test.com/oauth2', + 'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_AuthCode, + ); + + $response = $this->action("POST", "OAuth2ProviderController@token", + $params, + array(), + array(), + // Symfony interally prefixes headers with "HTTP", so + array("HTTP_Authorization" => " Basic " . base64_encode($this->client_id . ':' . $this->client_secret))); + + $status = $response->getStatusCode(); + + $this->assertResponseStatus(200); + + $content = $response->getContent(); + + $response = json_decode($content); + $access_token = $response->access_token; + $refresh_token = $response->refresh_token; + + $this->access_token = $access_token; + } + + /** + * @covers OAuth2UserApiController::get() + */ + public function testGetInfo(){ + $response = $this->action("GET", "OAuth2UserApiController@me", + array(), + array(), + array(), + array("HTTP_Authorization" => " Bearer " .$this->access_token)); + + $this->assertResponseStatus(200); + $content = $response->getContent(); + $user_info = json_decode($content); + } +} \ No newline at end of file diff --git a/app/validators/CustomValidator.php b/app/validators/CustomValidator.php index d555f9c9..b1acf8a8 100644 --- a/app/validators/CustomValidator.php +++ b/app/validators/CustomValidator.php @@ -71,4 +71,40 @@ class CustomValidator extends Validator { public function validateSslurl($attribute, $value, $parameters){ return preg_match(";^https:\/\/([\w@][\w.:@]+)\/?[\w\.?=%&=\-@/$,]*$;i",$value)==1; } + + public function validateFreeText($attribute, $value, $parameters){ + return preg_match('|^[a-z\-@_.,()\'"\s\:\/]+$|i',$value)==1; + } + + + public function validateSslorigin($attribute, $value, $parameters){ + if(filter_var($value, FILTER_VALIDATE_URL)){ + $parts = @parse_url($value); + + if ($parts === false) { + return false; + } + + if($parts['scheme']!=='https') + return false; + + if(isset($parts['query'])) + return false; + + if(isset($parts['fragment'])) + return false; + + if(isset($parts['path'])) + return false; + + if(isset($parts['user'])) + return false; + + if(isset($parts['pass'])) + return false; + + return true; + } + return false; + } } \ No newline at end of file diff --git a/app/views/admin/server-config.blade.php b/app/views/admin/server-config.blade.php index 96c68e42..fc4f7af2 100644 --- a/app/views/admin/server-config.blade.php +++ b/app/views/admin/server-config.blade.php @@ -13,10 +13,8 @@ General Configuration - - OPENID Configuration @@ -36,6 +34,7 @@ + @foreach($errors->all() as $message)

-

{{$app_name}} 

+

{{$app_name}} 

This app would like to: diff --git a/app/views/oauth2/consent.blade.php b/app/views/oauth2/consent.blade.php index 7752ab70..09c69d18 100644 --- a/app/views/oauth2/consent.blade.php +++ b/app/views/oauth2/consent.blade.php @@ -25,13 +25,13 @@

{{$app_name}} 

- +
This app would like to:
    @foreach ($requested_scopes as $scope) -
  • {{$scope->short_description}} 
  • +
  • {{$scope->short_description}} 
  • @endforeach

@@ -51,7 +51,7 @@ @stop diff --git a/app/views/oauth2/profile/edit-client-allowed-origins.blade.php b/app/views/oauth2/profile/edit-client-allowed-origins.blade.php new file mode 100644 index 00000000..44d77256 --- /dev/null +++ b/app/views/oauth2/profile/edit-client-allowed-origins.blade.php @@ -0,0 +1,158 @@ +

+
+ +
+
+

**Cannot contain a wildcard (http://*.example.com) or a path (http://example.com/subdir).

+
+ + + {{HTML::link(URL::action("ClientApiController@addAllowedOrigin",array("id"=>$client->id)),'Add',array('class'=>'btn add-origin-client','title'=>'Add a new Allowed Client Origin')) }} +
+
+
+ +
+
+ + + + + + + + + @foreach ($allowed_origins as $origin) + + + + + @endforeach + +
Allowed Origin 
{{ $origin->allowed_origin }} {{ HTML::link(URL::action("ClientApiController@deleteClientAllowedOrigin",array("id"=>$client->id,'origin_id'=>$origin->id)),'Delete',array('class'=>'btn del-allowed-origin','title'=>'Deletes a Allowed Origin')) }}
+ ** There are not any Registered Javascript Origins. +
+
+ +
+
+@section('scripts') +@parent + +@stop \ No newline at end of file diff --git a/app/views/oauth2/profile/edit-client-redirect-uris.blade.php b/app/views/oauth2/profile/edit-client-redirect-uris.blade.php index af51f6f2..fa79fe8c 100644 --- a/app/views/oauth2/profile/edit-client-redirect-uris.blade.php +++ b/app/views/oauth2/profile/edit-client-redirect-uris.blade.php @@ -3,20 +3,21 @@
+

** Redirect Uris they must been under SSL schema.

- + {{HTML::link(URL::action("ClientApiController@addAllowedRedirectUri",array("id"=>$client->id)),'Add',array('class'=>'btn add-uri-client','title'=>'Add a new Registered Client Uri')) }}
- @if (count($allowed_uris)>0) +
- +
- + @@ -29,9 +30,9 @@ @endforeach
Authorized UriAuthorized URI  
+ ** There are not any Authorized Redirect URIs.
- @endif @section('scripts') @@ -48,24 +49,31 @@ timeout:60000, success: function (data,textStatus,jqXHR) { //load data... - var uris = data.allowed_uris; - var template = $('
Delete'); - var directives = { - 'tr':{ - 'uri<-context':{ - 'td.uri-text':'uri.uri', - 'a.del-allowed-uri@href':function(arg){ - var uri_id = arg.item.id; - var href = '{{ URL::action("ClientApiController@deleteClientAllowedUri", array("id"=>$client->id,"uri_id"=>"-1")) }}'; - return href.replace('-1',uri_id); - } + if(uris.length>0){ + var template = $('Delete'); + var directives = { + 'tr':{ + 'uri<-context':{ + 'td.uri-text':'uri.uri', + 'a.del-allowed-uri@href':function(arg){ + var uri_id = arg.item.id; + var href = '{{ URL::action("ClientApiController@deleteClientAllowedUri", array("id"=>$client->id,"uri_id"=>"-1")) }}'; + return href.replace('-1',uri_id); + } + } } - } - }; - var html = template.render(uris, directives); - $('#body-allowed-uris').html(html.html()); + }; + var html = template.render(uris, directives); + $('#body-allowed-uris').html(html.html()); + $('#info-uris').hide(); + $('#table-uris').show(); + } + else{ + $('#info-uris').show(); + $('#table-uris').hide(); + } }, error: function (jqXHR, textStatus, errorThrown) { ajaxError(jqXHR, textStatus, errorThrown); @@ -76,6 +84,14 @@ $(document).ready(function() { + if($('#table-uris tr').length===1){ + $('#info-uris').show(); + $('#table-uris').hide(); + } + else{ + $('#info-uris').hide(); + $('#table-uris').show(); + } var form_add_redirect_uri = $('#form-add-uri'); diff --git a/app/views/oauth2/profile/edit-client-tokens.blade.php b/app/views/oauth2/profile/edit-client-tokens.blade.php index 8ecf77c9..163e3bae 100644 --- a/app/views/oauth2/profile/edit-client-tokens.blade.php +++ b/app/views/oauth2/profile/edit-client-tokens.blade.php @@ -5,13 +5,7 @@
-
- -
- - +
@@ -31,17 +25,13 @@ @endforeach
 Issued
+ ** There are not any Access Tokens granted for this application.

Issued Refresh Tokens

-
- -
@@ -66,7 +56,7 @@ @endforeach
- + ** There are not any Refresh Tokens granted for this application. @section('scripts') @parent @@ -172,11 +162,19 @@ $('#info-access-tokens').show(); $('#table-access-tokens').hide(); } + else{ + $('#info-access-tokens').hide(); + $('#table-access-tokens').show(); + } if($('#table-refresh-tokens tr').length===1){ $('#info-refresh-tokens').show(); $('#table-refresh-tokens').hide(); } + else{ + $('#info-refresh-tokens').hide(); + $('#table-refresh-tokens').show(); + } $("body").on('click','.refresh-refresh-tokens',function(event){ updateRefreshTokenList(); diff --git a/app/views/oauth2/profile/edit-client.blade.php b/app/views/oauth2/profile/edit-client.blade.php index 4458fc8f..d52946ea 100644 --- a/app/views/oauth2/profile/edit-client.blade.php +++ b/app/views/oauth2/profile/edit-client.blade.php @@ -4,7 +4,7 @@ @stop @section('content') @include('menu',array('is_oauth2_admin' => $is_oauth2_admin, 'is_openstackid_admin' => $is_openstackid_admin)) -Client {{ $client->app_name }} + {{$client->getFriendlyApplicationType()}} - Client {{ $client->app_name }} @if($errors->any())
    @@ -42,6 +42,20 @@
+ @if($client->application_type == oauth2\models\IClient::ApplicationType_JS_Client) +
+ +
+
+ @include('oauth2.profile.edit-client-allowed-origins',array('access_tokens' => $access_tokens, 'refresh_tokens' => $refresh_tokens,'client'=>$client,'allowed_uris'=>$allowed_uris,'allowed_origins'=>$allowed_origins)) +
+
+
+ @endif
diff --git a/bootstrap/start.php b/bootstrap/start.php index 8cc6b706..90ccd8de 100644 --- a/bootstrap/start.php +++ b/bootstrap/start.php @@ -1,5 +1,8 @@ app->make('auth\\IAuthenticationExtensionService')), + new CustomAuthProvider( + ServiceLocator::getInstance()->getService('auth\\IAuthenticationExtensionService'), + ServiceLocator::getInstance()->getService(OpenIdServiceCatalog::UserService), + ServiceLocator::getInstance()->getService(UtilsServiceCatalog::CheckPointService) + ), App::make('session.store') ); }); diff --git a/public/css/main.css b/public/css/main.css index 676a6846..b53631fa 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -8,6 +8,7 @@ text-indent: -1000em; width: 177px; } + .panel-heading { background-color: #F5F5F5; border-bottom: 1px solid #DDDDDD; @@ -16,6 +17,7 @@ margin: -15px -15px 15px; padding: 10px 15px; } + .panel { background-color: #FFFFFF; border: 1px solid #DDDDDD; @@ -24,24 +26,30 @@ margin-bottom: 20px; padding: 15px; } + * { -moz-box-sizing: border-box; } + .margin-top-20 { margin-top: 20px; } -select, input[type="text"]{ + +.modal-body select, .modal-body input[type="text"]{ height: 30px !important; width: 100%; } + textarea { width: 100%; } -#redirect_uri{ +#redirect_uri, #origin{ margin-bottom: 0px!important; + height: 30px !important; + width: 90% !important; } #dialog-form-register-new-app textarea { @@ -66,9 +74,6 @@ textarea { padding-bottom: 1px; } -#redirect_uri{ - width: 100%; -} .popover-content{ font-size: 12px; diff --git a/public/js/jquery.validate.additional.custom.methods.js b/public/js/jquery.validate.additional.custom.methods.js index 20a5b6a9..c5348c5e 100644 --- a/public/js/jquery.validate.additional.custom.methods.js +++ b/public/js/jquery.validate.additional.custom.methods.js @@ -16,9 +16,15 @@ jQuery.validator.addMethod("scopename", function(value, element) { }, "please enter a valid scope name."); jQuery.validator.addMethod("free_text", function(value, element) { - return this.optional(element) || /^[a-z\-.,()'"\s\/]+$/i.test(value); + return this.optional(element) || /^[a-z\-_.,()'"\s\:\/]+$/i.test(value); }, "Letters or punctuation only please"); +jQuery.validator.addMethod("endpointroute", function(value, element) { + return this.optional(element) || /^\/([\w@][\w.:@]+)\/?[\w\.?=%&=\-@/$,{}]*$/ig.test(value); +}, "please enter a valid endpoint route"); + + + var showCustomLabel = function ( element, message ) { var label = this.errorsFor( element );