diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..dc5cd0e1 --- /dev/null +++ b/.env.example @@ -0,0 +1,46 @@ +APP_ENV=local +APP_DEBUG=true +APP_KEY=SomeRandomString +APP_URL=http://localhost +APP_OAUTH_2_0_CLIENT_ID=clientid +APP_OAUTH_2_0_CLIENT_SECRET=clientsecret +APP_OAUTH_2_0_AUTH_SERVER_BASE_URL=http://localhost + +DB_HOST=localhost +DB_DATABASE=homestead +DB_USERNAME=homestead +DB_PASSWORD=secret + +SS_DB_HOST=localhost +SS_DB_DATABASE=homestead +SS_DB_USERNAME=homestead +SS_DB_PASSWORD=secret + +REDIS_HOST=127.0.0.1 +REDIS_PORT=port +REDIS_DB=0 +REDIS_PASSWORD= + +CACHE_DRIVER=file + +SESSION_DRIVER=redis +SESSION_COOKIE_DOMAIN= +SESSION_COOKIE_SECURE=false + +QUEUE_DRIVER=sync + +MAIL_DRIVER=smtp +MAIL_HOST=mailtrap.io +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null + +CORS_ALLOWED_HEADERS=origin, content-type, accept, authorization, x-requested-with +CORS_ALLOWED_METHODS=GET, POST, OPTIONS, PUT, DELETE +CORS_USE_PRE_FLIGHT_CACHING=true +CORS_MAX_AGE=3200 +CORS_EXPOSED_HEADERS= + +CURL_TIMEOUT=60 +CURL_ALLOWS_REDIRECT=false +CURL_VERIFY_SSL_CERT=true \ No newline at end of file diff --git a/.env.testing b/.env.testing new file mode 100644 index 00000000..0119b429 --- /dev/null +++ b/.env.testing @@ -0,0 +1,50 @@ +APP_ENV=testing +APP_DEBUG=true +APP_KEY=KKzP6APRNHmADURQ8OanDTU5kDpGwo6l +APP_URL=https://local.resource-server.openstack.org +APP_OAUTH_2_0_CLIENT_ID=tM9iYEq2iCP6P5WQL.~Zo2XXLbugpNhu.openstack.client +APP_OAUTH_2_0_CLIENT_SECRET=f70Ydbhq9NernTem4Yow8SEB +APP_OAUTH_2_0_AUTH_SERVER_BASE_URL=https://local.openstackid.openstack.org + +DB_HOST=localhost +DB_DATABASE=resource_server_test +DB_USERNAME=root +DB_PASSWORD=Koguryo@1981 + +SS_DB_HOST=localhost +SS_DATABASE=os_local +SS_DB_USERNAME=root +SS_DB_PASSWORD=Koguryo@1981 + +REDIS_HOST=127.0.0.1 +REDIS_PORT=6379 +REDIS_DB=0 +REDIS_PASSWORD= + +CACHE_DRIVER=redis + +SESSION_DRIVER=redis +SESSION_COOKIE_DOMAIN= +SESSION_COOKIE_SECURE=false + +QUEUE_DRIVER=sync + +MAIL_DRIVER=smtp +MAIL_HOST=mailtrap.io +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null + + +LOG_EMAIL_TO= +LOG_EMAIL_FROM= + +CORS_ALLOWED_HEADERS=origin, content-type, accept, authorization, x-requested-with +CORS_ALLOWED_METHODS=GET, POST, OPTIONS, PUT, DELETE +CORS_USE_PRE_FLIGHT_CACHING=false +CORS_MAX_AGE=3200 +CORS_EXPOSED_HEADERS= + +CURL_TIMEOUT=3600 +CURL_ALLOWS_REDIRECT=false +CURL_VERIFY_SSL_CERT=false \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..95883dea --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +* text=auto +*.css linguist-vendored +*.less linguist-vendored diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..f352365b --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +/vendor +/node_modules +.env +composer.phar +composer.lock +.DS_Storeapp/storage +/app/storage/* +.idea/* +app/config/dev/* +app/config/testing/* +app/config/local/* +app/config/production/* +app/config/staging/* +app/config/packages/greggilbert/recaptcha/dev/* +app/config/packages/greggilbert/recaptcha/local/* +app/config/packages/greggilbert/recaptcha/production/* +app/config/packages/greggilbert/recaptcha/staging/* +/bootstrap/compiled.php +/bootstrap/environment.php +.tox +AUTHORS +ChangeLog +doc/build +*.egg +*.egg-info + + +.env.testing \ No newline at end of file diff --git a/app/Commands/Command.php b/app/Commands/Command.php new file mode 100644 index 00000000..018bc219 --- /dev/null +++ b/app/Commands/Command.php @@ -0,0 +1,7 @@ +comment(PHP_EOL.Inspiring::quote().PHP_EOL); + } + +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php new file mode 100644 index 00000000..0c088c89 --- /dev/null +++ b/app/Console/Kernel.php @@ -0,0 +1,29 @@ +command('inspire') + ->hourly(); + } + +} diff --git a/app/Events/Event.php b/app/Events/Event.php new file mode 100644 index 00000000..d59f7690 --- /dev/null +++ b/app/Events/Event.php @@ -0,0 +1,7 @@ + 'server error'), 500); + } + + protected function created($data = 'ok') + { + $res = Response::json($data, 201); + //jsonp + if (Input::has('callback')) + { + $res->setCallback(Input::get('callback')); + } + return $res; + } + + protected function deleted($data = 'ok') + { + $res = Response::json($data, 204); + //jsonp + if (Input::has('callback')) + { + $res->setCallback(Input::get('callback')); + } + return $res; + } + + protected function ok($data = 'ok') + { + $res = Response::json($data, 200); + //jsonp + if (Input::has('callback')) + { + $res->setCallback(Input::get('callback')); + } + return $res; + } + + protected function error400($data) + { + return Response::json($data, 400); + } + + protected function error404($data = array('message' => 'Entity Not Found')) + { + return Response::json($data, 404); + } + + /** + * { + "message": "Validation Failed", + "errors": [ + { + "resource": "Issue", + "field": "title", + "code": "missing_field" + } + ] + } + * @param $messages + * @return mixed + */ + protected function error412($messages) + { + return Response::json(array('message' => 'Validation Failed', 'errors' => $messages), 412); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/OAuth2ProtectedController.php b/app/Http/Controllers/OAuth2ProtectedController.php new file mode 100644 index 00000000..190a6fb4 --- /dev/null +++ b/app/Http/Controllers/OAuth2ProtectedController.php @@ -0,0 +1,40 @@ +resource_server_context = $resource_server_context; + } + +} \ No newline at end of file diff --git a/app/Http/Controllers/apis/protected/marketplace/OAuth2CloudApiController.php b/app/Http/Controllers/apis/protected/marketplace/OAuth2CloudApiController.php new file mode 100644 index 00000000..f99ae4f3 --- /dev/null +++ b/app/Http/Controllers/apis/protected/marketplace/OAuth2CloudApiController.php @@ -0,0 +1,86 @@ +getCompanyServices(); + } + + /** + * @param $id + * @return mixed + */ + public function getCloud($id) + { + return $this->getCompanyService($id); + } + + /** + * @param $id + * @return mixed + */ + public function getCloudDataCenters($id) + { + try { + $cloud = $this->repository->getById($id); + + if (!$cloud) + { + return $this->error404(); + } + + $data_center_regions = $cloud->datacenters_regions(); + + $res = array(); + + foreach ($data_center_regions as $region) + { + $data = $region->toArray(); + $locations = $region->locations(); + $data_locations = array(); + foreach ($locations as $loc) + { + array_push($data_locations, $loc->toArray()); + } + $data['locations'] = $data_locations; + array_push($res, $data); + } + + return $this->ok(array('datacenters' => $res )); + } + catch (Exception $ex) + { + Log::error($ex); + return $this->error500($ex); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/apis/protected/marketplace/OAuth2CompanyServiceApiController.php b/app/Http/Controllers/apis/protected/marketplace/OAuth2CompanyServiceApiController.php new file mode 100644 index 00000000..8f264beb --- /dev/null +++ b/app/Http/Controllers/apis/protected/marketplace/OAuth2CompanyServiceApiController.php @@ -0,0 +1,143 @@ + 'The :attribute field is does not has a valid value (all, active, non_active).', + 'order' => 'The :attribute field is does not has a valid value (date, name).', + 'order_dir' => 'The :attribute field is does not has a valid value (desc, asc).', + ); + + $rules = array( + 'page' => 'integer|min:1', + 'per_page' => 'required_with:page|integer|min:10|max:100', + 'status' => 'status', + 'order_by' => 'order', + 'order_dir' => 'required_with:order_by|order_dir', + ); + // Creates a Validator instance and validates the data. + $validation = Validator::make($values, $rules, $messages); + + if ($validation->fails()) + { + $messages = $validation->messages()->toArray(); + return $this->error412($messages); + } + + if (Input::has('page')) + { + $page = intval(Input::get('page')); + $per_page = intval(Input::get('per_page')); + } + + if (Input::has('status')) + { + $status = Input::get('status'); + } + + if (Input::has('order_by')) + { + $order_by = Input::get('order_by'); + $order_dir = Input::get('order_dir'); + } + + $data = $this->repository->getAll($page, $per_page, $status, $order_by, $order_dir); + return $this->ok($data); + } + catch (Exception $ex) + { + Log::error($ex); + return $this->error500($ex); + } + } + + /** + * @param $id + * @return mixed + */ + public function getCompanyService($id) + { + try + { + $data = $this->repository->getById($id); + return ($data)? $this->ok($data) : $this->error404(); + } + catch (Exception $ex) + { + Log::error($ex); + return $this->error500($ex); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/apis/protected/marketplace/OAuth2ConsultantsApiController.php b/app/Http/Controllers/apis/protected/marketplace/OAuth2ConsultantsApiController.php new file mode 100644 index 00000000..286f34f4 --- /dev/null +++ b/app/Http/Controllers/apis/protected/marketplace/OAuth2ConsultantsApiController.php @@ -0,0 +1,89 @@ +repository = $repository; + } + + /** + * query string params: + * page: You can specify further pages + * per_page: custom page size up to 100 ( min 10) + * status: cloud status ( active , not active, all) + * order_by: order by field + * order_dir: order direction + * @return mixed + */ + public function getConsultants() + { + return $this->getCompanyServices(); + } + + /** + * @param $id + * @return mixed + */ + public function getConsultant($id) + { + return $this->getCompanyService($id); + } + + /** + * @param $id + * @return mixed + */ + public function getOffices($id) + { + try + { + $consultant = $this->repository->getById($id); + + if (!$consultant) + { + return $this->error404(); + } + + $offices = $consultant->offices(); + $res = array(); + + foreach ($offices as $office) + { + array_push($res, $office->toArray()); + } + return $this->ok(array('offices' => $res)); + } + catch (Exception $ex) + { + Log::error($ex); + return $this->error500($ex); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/apis/protected/marketplace/OAuth2PrivateCloudApiController.php b/app/Http/Controllers/apis/protected/marketplace/OAuth2PrivateCloudApiController.php new file mode 100644 index 00000000..f1a40b48 --- /dev/null +++ b/app/Http/Controllers/apis/protected/marketplace/OAuth2PrivateCloudApiController.php @@ -0,0 +1,36 @@ +repository = $repository; + } +} \ No newline at end of file diff --git a/app/Http/Controllers/apis/protected/marketplace/OAuth2PublicCloudApiController.php b/app/Http/Controllers/apis/protected/marketplace/OAuth2PublicCloudApiController.php new file mode 100644 index 00000000..29049015 --- /dev/null +++ b/app/Http/Controllers/apis/protected/marketplace/OAuth2PublicCloudApiController.php @@ -0,0 +1,30 @@ +repository = $repository; + } +} \ No newline at end of file diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php new file mode 100644 index 00000000..7d5d09f7 --- /dev/null +++ b/app/Http/Kernel.php @@ -0,0 +1,37 @@ + 'App\Http\Middleware\Authenticate', + 'auth.basic' => 'Illuminate\Auth\Middleware\AuthenticateWithBasicAuth', + 'guest' => 'App\Http\Middleware\RedirectIfAuthenticated', + 'oauth2.protected' => 'App\Http\Middleware\OAuth2BearerAccessTokenRequestValidator', + 'rate.limit' => 'App\Http\Middleware\RateLimitMiddleware', + 'etags' => 'App\Http\Middleware\ETagsMiddleware', + ]; + +} \ No newline at end of file diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php new file mode 100644 index 00000000..72a7613a --- /dev/null +++ b/app/Http/Middleware/Authenticate.php @@ -0,0 +1,50 @@ +auth = $auth; + } + + /** + * Handle an incoming request. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * @return mixed + */ + public function handle($request, Closure $next) + { + if ($this->auth->guest()) + { + if ($request->ajax()) + { + return response('Unauthorized.', 401); + } + else + { + return redirect()->guest('auth/login'); + } + } + + return $next($request); + } + +} diff --git a/app/Http/Middleware/CORSMiddleware.php b/app/Http/Middleware/CORSMiddleware.php new file mode 100644 index 00000000..d4dda0b5 --- /dev/null +++ b/app/Http/Middleware/CORSMiddleware.php @@ -0,0 +1,505 @@ +endpoint_repository = $endpoint_repository; + $this->cache_service = $cache_service; + $this->allowed_headers = Config::get('cors.allowed_headers', self::DefaultAllowedHeaders); + $this->allowed_methods = Config::get('cors.allowed_methods', self::DefaultAllowedMethods); + } + + /** + * Handle an incoming request. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * @return mixed + */ + public function handle($request, Closure $next) + { + if ($response = $this->preProcess($request)) + { + return $response; + } + //normal processing + $response = $next($request); + $this->postProcess($request, $response); + return $response; + } + + private function generatePreflightCacheKey($request) + { + $cache_id = 'pre-flight-'. $request->getClientIp(). '-' . $request->getRequestUri(). '-' . $request->getMethod(); + return $cache_id; + } + + /** + * @param Request $request + * @return Response + */ + public function preProcess(Request $request) + { + $actual_request = false; + if ($this->isValidCORSRequest($request)) + { + if (!$this->testOriginHeaderScrutiny($request)) + { + $response = new Response(); + $response->setStatusCode(403); + return $response; + } + /* Step 01 : Determine the type of the incoming request */ + $type = $this->getRequestType($request); + /* Step 02 : Process request according to is type */ + switch($type) + { + case CORSRequestPreflightType::REQUEST_FOR_PREFLIGHT: + { + // HTTP request send by client to preflight a further 'Complex' request + // sets the original method on request in order to be able to find the + // correct route + $real_method = $request->headers->get('Access-Control-Request-Method'); + $request->setMethod($real_method); + + $route_path = RequestUtils::getCurrentRoutePath($request); + if (!$route_path || !$this->checkEndPoint($route_path, $real_method)) + { + $response = new Response(); + $response->setStatusCode(403); + return $response; + } + // ----Step 2b: Store pre-flight request data in the Cache to keep (mark) the request as correctly followed the request pre-flight process + $data = new CORSRequestPreflightData($request, $this->current_endpoint->supportCredentials()); + $cache_id = $this->generatePreflightCacheKey($request); + $this->cache_service->storeHash($cache_id, $data->toArray(), CORSRequestPreflightData::$cache_lifetime); + // ----Step 2c: Return corresponding response - This part should be customized with application specific constraints..... + return $this->makePreflightResponse($request); + } + break; + case CORSRequestPreflightType::COMPLEX_REQUEST: + { + $cache_id = $this->generatePreflightCacheKey($request); +; // ----Step 2a: Check if the current request has an entry into the preflighted requests Cache + $data = $this->cache_service->getHash($cache_id, CORSRequestPreflightData::$cache_attributes); + if (!count($data)) + { + $response = new Response(); + $response->setStatusCode(403); + return $response; + } + // ----Step 2b: Check that pre-flight information declared during the pre-flight request match the current request on key information + $match = false; + // ------Start with comparison of "Origin" HTTP header (according to utility method impl. used to retrieve header reference cannot be null)... + if ($request->headers->get('Origin') === $data['origin']) + { + // ------Continue with HTTP method... + if ($request->getMethod() === $data['expected_method']) + { + // ------Finish with custom HTTP headers (use an method to avoid manual iteration on collection to increase the speed)... + $x_headers = self::getCustomHeaders($request); + $x_headers_pre = explode(',', $data['expected_custom_headers']); + sort($x_headers); + sort($x_headers_pre); + if (count(array_diff($x_headers, $x_headers_pre)) === 0) + { + $match = true; + } + } + } + if (!$match) + { + $response = new Response(); + $response->setStatusCode(403); + return $response; + } + $actual_request = true; + } + break; + case CORSRequestPreflightType::SIMPLE_REQUEST: + { + // origins, do not set any additional headers and terminate this set of steps. + if (!$this->isAllowedOrigin($request)) { + $response = new Response(); + $response->setStatusCode(403); + + return $response; + } + $actual_request = true; + // If the resource supports credentials add a single Access-Control-Allow-Origin header, with the value + // of the Origin header as value, and add a single Access-Control-Allow-Credentials header with the + // case-sensitive string "true" as value. + // Otherwise, add a single Access-Control-Allow-Origin header, with either the value of the Origin header + // or the string "*" as value. + } + break; + } + } + if ($actual_request) + { + // Save response headers + $cache_id = $this->generatePreflightCacheKey($request); + // ----Step 2a: Check if the current request has an entry into the preflighted requests Cache + $data = $this->cache_service->getHash($cache_id, CORSRequestPreflightData::$cache_attributes); + $this->headers['Access-Control-Allow-Origin'] = $request->headers->get('Origin'); + if ((bool)$data['allows_credentials']) + { + $this->headers['Access-Control-Allow-Credentials'] = 'true'; + } + /** + * During a CORS request, the getResponseHeader() method can only access simple response headers. + * Simple response headers are defined as follows: + ** Cache-Control + ** Content-Language + ** Content-Type + ** Expires + ** Last-Modified + ** Pragma + * If you want clients to be able to access other headers, + * you have to use the Access-Control-Expose-Headers header. + * The value of this header is a comma-delimited list of response headers you want to expose + * to the client. + */ + $exposed_headers = Config::get('cors.exposed_headers', 'Content-Type, Expires'); + if (!empty($exposed_headers)) + { + $this->headers['Access-Control-Expose-Headers'] = $exposed_headers ; + } + } + } + + public function postProcess(Request $request, Response $response) + { + // add CORS response headers + if (count($this->headers) > 0) + { + $response->headers->add($this->headers); + } + return $response; + } + + /** + * @param Request $request + * @return Response + */ + private function makePreflightResponse(Request $request) + { + $response = new Response(); + if (!$this->isAllowedOrigin($request)) + { + $response->headers->set('Access-Control-Allow-Origin', 'null'); + $response->setStatusCode(403); + return $response; + } + $response->headers->set('Access-Control-Allow-Origin', $request->headers->get('Origin')); + // The Access-Control-Request-Method header indicates which method will be used in the actual + // request as part of the preflight request + // check request method + if ($request->headers->get('Access-Control-Request-Method') != $this->current_endpoint->getHttpMethod()) + { + $response->setStatusCode(405); + return $response; + } + // The Access-Control-Allow-Credentials header indicates whether the response to request + // can be exposed when the omit credentials flag is unset. When part of the response to a preflight request + // it indicates that the actual request can include user credentials. + if ( $this->current_endpoint->supportCredentials()) + { + $response->headers->set('Access-Control-Allow-Credentials', 'true'); + } + if (Config::get('cors.use_pre_flight_caching', false)) + { + // The Access-Control-Max-Age header indicates how long the response can be cached, so that for + // subsequent requests, within the specified time, no preflight request has to be made. + $response->headers->set('Access-Control-Max-Age', Config::get('cors.max_age', 32000)); + } + // The Access-Control-Allow-Headers header indicates, as part of the response to a preflight request, + // which header field names can be used during the actual request + $response->headers->set('Access-Control-Allow-Headers', $this->allowed_headers); + + //The Access-Control-Allow-Methods header indicates, as part of the response to a preflight request, + // which methods can be used during the actual request. + $response->headers->set('Access-Control-Allow-Methods', $this->allowed_methods); + // The Access-Control-Request-Headers header indicates which headers will be used in the actual request + // as part of the preflight request. + $headers = $request->headers->get('Access-Control-Request-Headers'); + if ($headers) + { + $headers = trim(strtolower($headers)); + $allow_headers = explode(', ', $this->allowed_headers); + foreach (preg_split('{, *}', $headers) as $header) + { + //if they are simple headers then skip them + if (in_array($header, self::$simple_headers, true)) + { + continue; + } + //check is the requested header is on the list of allowed headers + if (!in_array($header, $allow_headers, true)) + { + $response->setStatusCode(400); + $response->setContent('Unauthorized header '.$header); + break; + } + } + } + //OK - No Content + $response->setStatusCode(204); + return $response; + } + + /** + * @param Request $request + * @returns bool + */ + private function isValidCORSRequest(Request $request) + { + /** + * The presence of the Origin header does not necessarily mean that the request is a cross-origin request. + * While all cross-origin requests will contain an Origin header, + + * Origin header on same-origin requests. But Chrome and Safari include an Origin header on + * same-origin POST/PUT/DELETE requests (same-origin GET requests will not have an Origin header). + */ + return $request->headers->has('Origin'); + } + + /** + * https://www.owasp.org/index.php/CORS_OriginHeaderScrutiny + * Filter that will ensure the following points for each incoming HTTP CORS requests: + * - Have only one and non empty instance of the origin header, + * - Have only one and non empty instance of the host header, + * - The value of the origin header is present in a internal allowed domains list (white list). As we act before the + * step 2 of the CORS HTTP requests/responses exchange process, allowed domains list is yet provided to client, + * - Cache IP of the sender for 1 hour. If the sender send one time a origin domain that is not in the white list + * then all is requests will return an HTTP 403 response (protract allowed domain guessing). + * We use the method above because it's not possible to identify up to 100% that the request come from one expected + * client application, since: + * - All information of a HTTP request can be faked, + * - It's the browser (or others tools) that send the HTTP request then the IP address that we have access to is the + * client IP address. + * @param Request $request + * @return bool + */ + private function testOriginHeaderScrutiny(Request $request) + { + /* Step 0 : Check presence of client IP in black list */ + $client_ip = $request->getClientIp(); + if (Cache::has(self::CORS_IP_BLACKLIST_PREFIX . $client_ip)) + { + return false; + } + /* Step 1 : Check that we have only one and non empty instance of the "Origin" header */ + $origin = $request->headers->get('Origin', null, false); + if (is_array($origin) && count($origin) > 1) + { + // If we reach this point it means that we have multiple instance of the "Origin" header + // Add client IP address to black listed client + $expiresAt = Carbon::now()->addMinutes(60); + Cache::put(self::CORS_IP_BLACKLIST_PREFIX . $client_ip, self::CORS_IP_BLACKLIST_PREFIX . $client_ip, $expiresAt); + return false; + } + /* Step 2 : Check that we have only one and non empty instance of the "Host" header */ + $host = $request->headers->get('Host', null, false); + //Have only one and non empty instance of the host header, + if (is_array($host) && count($host) > 1) + { + // If we reach this point it means that we have multiple instance of the "Host" header + $expiresAt = Carbon::now()->addMinutes(60); + Cache::put(self::CORS_IP_BLACKLIST_PREFIX . $client_ip, self::CORS_IP_BLACKLIST_PREFIX . $client_ip, $expiresAt); + return false; + } + /* Step 3 : Perform analysis - Origin header is required */ + + $origin = $request->headers->get('Origin'); + $host = $request->headers->get('Host'); + $server_name = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : null; + $origin_host = @parse_url($origin, PHP_URL_HOST); + + + // check origin not empty and allowed + + if (!$this->isAllowedOrigin($origin)) + { + $expiresAt = Carbon::now()->addMinutes(60); + Cache::put(self::CORS_IP_BLACKLIST_PREFIX . $client_ip, self::CORS_IP_BLACKLIST_PREFIX . $client_ip, $expiresAt); + return false; + } + + if (is_null($host) || $server_name != $host || is_null($origin_host) || $origin_host == $server_name) + { + $expiresAt = Carbon::now()->addMinutes(60); + Cache::put(self::CORS_IP_BLACKLIST_PREFIX . $client_ip, self::CORS_IP_BLACKLIST_PREFIX . $client_ip, $expiresAt); + return false; + } + + /* Step 4 : Finalize request next step */ + return true; + } + + private function checkEndPoint($endpoint_path, $http_method) + { + $this->current_endpoint = $this->endpoint_repository->getApiEndpointByUrlAndMethod($endpoint_path, $http_method); + if (is_null($this->current_endpoint)) + { + return false; + } + if (!$this->current_endpoint->supportCORS() || !$this->current_endpoint->isActive()) + { + return false; + } + return true; + } + + /** + * @param string $origin + * @return bool + */ + private function isAllowedOrigin($origin) + { + return true; + } + + private static function getRequestType(Request $request) + { + + $type = CORSRequestPreflightType::UNKNOWN; + $http_method = $request->getMethod(); + $content_type = strtolower($request->getContentType()); + $http_method = strtoupper($http_method); + + if ($http_method === 'OPTIONS' && $request->headers->has('Access-Control-Request-Method')) + { + $type = CORSRequestPreflightType::REQUEST_FOR_PREFLIGHT; + } + else + { + if (self::hasCustomHeaders($request)) + { + $type = CORSRequestPreflightType::COMPLEX_REQUEST; + } + elseif ($http_method === 'POST' && !in_array($content_type, self::$simple_content_header_values, true)) + { + $type = CORSRequestPreflightType::COMPLEX_REQUEST; + } + elseif (!in_array($http_method, self::$simple_http_methods, true)) + { + $type = CORSRequestPreflightType::COMPLEX_REQUEST; + } + else + { + $type = CORSRequestPreflightType::SIMPLE_REQUEST; + } + } + return $type; + } + + + private static function getCustomHeaders(Request $request) + { + $custom_headers = array(); + foreach ($request->headers->all() as $k => $h) + { + if (starts_with('X-', strtoupper(trim($k)))) + { + array_push($custom_headers, strtoupper(trim($k))); + } + } + return $custom_headers; + } + + private static function hasCustomHeaders(Request $request) + { + return count(self::getCustomHeaders($request)) > 0; + } +} \ No newline at end of file diff --git a/app/Http/Middleware/CORSRequestPreflightData.php b/app/Http/Middleware/CORSRequestPreflightData.php new file mode 100644 index 00000000..899dad76 --- /dev/null +++ b/app/Http/Middleware/CORSRequestPreflightData.php @@ -0,0 +1,82 @@ +sender = $request->getClientIp(); + $this->uri = $request->getRequestUri(); + $this->origin = $request->headers->get('Origin'); + $this->expected_method = $request->headers->get('Access-Control-Request-Method'); + $this->allows_credentials = $allows_credentials; + + $tmp = $request->headers->get("Access-Control-Request-Headers"); + if (!empty($tmp)) + { + $hs = explode(',', $tmp); + foreach ($hs as $h) + { + array_push($this->expected_custom_headers, strtoupper(trim($h))); + } + } + } + + /** + * @return array + */ + public function toArray() + { + $res = array(); + $res['sender'] = $this->sender; + $res['uri'] = $this->uri; + $res['origin'] = $this->origin; + $res['allows_credentials'] = $this->allows_credentials; + $res['expected_method'] = $this->expected_method; + $res['expected_custom_headers'] = implode(',', $this->expected_custom_headers); + return $res; + } + +} \ No newline at end of file diff --git a/app/Http/Middleware/CORSRequestPreflightType.php b/app/Http/Middleware/CORSRequestPreflightType.php new file mode 100644 index 00000000..4dcf2a82 --- /dev/null +++ b/app/Http/Middleware/CORSRequestPreflightType.php @@ -0,0 +1,36 @@ +getStatusCode() === 200) + { + $etag = md5($response->getContent()); + $requestETag = str_replace('"', '', $request->getETags()); + if ($requestETag && $requestETag[0] == $etag) + { + $response->setNotModified(); + } + $response->setEtag($etag); + } + return $response; + } +} \ No newline at end of file diff --git a/app/Http/Middleware/OAuth2BearerAccessTokenRequestValidator.php b/app/Http/Middleware/OAuth2BearerAccessTokenRequestValidator.php new file mode 100644 index 00000000..d456b534 --- /dev/null +++ b/app/Http/Middleware/OAuth2BearerAccessTokenRequestValidator.php @@ -0,0 +1,286 @@ +context = $context; + $this->headers = $this->getHeaders(); + $this->endpoint_repository = $endpoint_repository; + $this->token_service = $token_service; + } + + /** + * @param \Illuminate\Http\Request $request + * @param callable $next + * @return OAuth2WWWAuthenticateErrorResponse + */ + public function handle($request, Closure $next) + { + $url = $request->getRequestUri(); + $method = $request->getMethod(); + $realm = $request->getHost(); + + try + { + $route = RequestUtils::getCurrentRoutePath($request); + if (!$route) + { + throw new OAuth2ResourceServerException( + 400, + OAuth2Protocol::OAuth2Protocol_Error_InvalidRequest, + sprintf('API endpoint does not exits! (%s:%s)', $url, $method) + ); + } + // http://tools.ietf.org/id/draft-abarth-origin-03.html + $origin = $request->headers->has('Origin') ? $request->headers->get('Origin') : null; + if(!empty($origin)) + { + $nm = new Normalizer($origin); + $origin = $nm->normalize(); + } + + //check first http basic auth header + $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 + { + // http://tools.ietf.org/html/rfc6750#section-2- 2 + // if access token is not on authorization header check on POST/GET params + $access_token_value = Input::get(OAuth2Protocol::OAuth2Protocol_AccessToken, ''); + } + + if (is_null($access_token_value) || empty($access_token_value)) + { + //if access token value is not set, then error + throw new OAuth2ResourceServerException( + 400, + OAuth2Protocol::OAuth2Protocol_Error_InvalidRequest, + 'missing access token' + ); + } + + $endpoint = $this->endpoint_repository->getApiEndpointByUrlAndMethod($route, $method); + + //api endpoint must be registered on db and active + if (is_null($endpoint) || !$endpoint->isActive()) + { + throw new OAuth2ResourceServerException( + 400, + OAuth2Protocol::OAuth2Protocol_Error_InvalidRequest, + sprintf('API endpoint does not exits! (%s:%s)', $route, $method) + ); + } + + $token_info = $this->token_service->get($access_token_value); + + //check lifetime + if (is_null($token_info) || $token_info->getLifetime() <= 0) + { + throw new OAuth2ResourceServerException( + 401, + OAuth2Protocol::OAuth2Protocol_Error_UnauthorizedClient, + 'invalid origin' + ); + } + //check token audience + $audience = explode(' ', $token_info->getAudience()); + if ((!in_array($realm, $audience))) + { + throw new OAuth2ResourceServerException( + 401, + OAuth2Protocol::OAuth2Protocol_Error_InvalidToken, + 'the access token provided is expired, revoked, malformed, or invalid for other reasons.' + ); + } + if ($token_info->getApplicationType() === 'JS_CLIENT' && str_contains($token_info->getAllowedOrigins(), $origin) === false) + { + //check origins + throw new OAuth2ResourceServerException( + 403, + OAuth2Protocol::OAuth2Protocol_Error_UnauthorizedClient, + 'invalid origin' + ); + } + //check scopes + $endpoint_scopes = explode(' ', $endpoint->getScope()); + $token_scopes = explode(' ', $token_info->getScope()); + //check token available scopes vs. endpoint scopes + if (count(array_intersect($endpoint_scopes, $token_scopes)) == 0) + { + Log::error( + sprintf( + 'access token scopes (%s) does not allow to access to api url %s , needed scopes %s', + $token_info->getScope(), + $url, + implode(' OR ', $endpoint_scopes) + ) + ); + + throw new OAuth2ResourceServerException( + 403, + OAuth2Protocol::OAuth2Protocol_Error_InsufficientScope, + 'the request requires higher privileges than provided by the access token', + implode(' ', $endpoint_scopes) + ); + } + //set context for api and continue processing + $context = array( + 'access_token' => $access_token_value, + 'expires_in' => $token_info->getLifetime(), + 'client_id' => $token_info->getClientId(), + 'scope' => $token_info->getScope() + ); + + if (!is_null($token_info->getUserId())) + { + $context['user_id'] = $token_info->getUserId(); + } + $this->context->setAuthorizationContext($context); + } + catch (OAuth2ResourceServerException $ex1) + { + Log::error($ex1); + $response = new OAuth2WWWAuthenticateErrorResponse( + $realm, + $ex1->getError(), + $ex1->getErrorDescription(), + $ex1->getScope(), + $ex1->getHttpCode() + ); + $http_response = Response::json($response->getContent(), $response->getHttpCode()); + $http_response->header('WWW-Authenticate', $response->getWWWAuthenticateHeaderValue()); + return $http_response; + } + catch (InvalidGrantTypeException $ex2) + { + Log::error($ex2); + $response = new OAuth2WWWAuthenticateErrorResponse( + $realm, + OAuth2Protocol::OAuth2Protocol_Error_InvalidToken, + 'the access token provided is expired, revoked, malformed, or invalid for other reasons.', + null, + 401 + ); + $http_response = Response::json($response->getContent(), $response->getHttpCode()); + $http_response->header('WWW-Authenticate', $response->getWWWAuthenticateHeaderValue()); + return $http_response; + } + catch (\Exception $ex) + { + Log::error($ex); + $response = new OAuth2WWWAuthenticateErrorResponse( + $realm, + OAuth2Protocol::OAuth2Protocol_Error_InvalidRequest, + 'invalid request', + null, + 400 + ); + $http_response = Response::json($response->getContent(), $response->getHttpCode()); + $http_response->header('WWW-Authenticate', $response->getWWWAuthenticateHeaderValue()); + return $http_response; + } + $response = $next($request); + return $response; + } + + /** + * @return array + */ + protected function getHeaders() + { + $headers = array(); + if (function_exists('getallheaders')) + { + foreach (getallheaders() as $name => $value) + { + $headers[strtolower($name)] = $value; + } + } + else + { + // @codeCoverageIgnoreEnd + foreach ($_SERVER as $name => $value) + { + if (substr($name, 0, 5) == 'HTTP_') + { + $name = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5))))); + $headers[strtolower($name)] = $value; + } + } + foreach (Request::header() as $name => $value) + { + if (!array_key_exists($name, $headers)) + { + $headers[strtolower($name)] = $value[0]; + } + } + } + return $headers; + } +} \ No newline at end of file diff --git a/app/Http/Middleware/RateLimitMiddleware.php b/app/Http/Middleware/RateLimitMiddleware.php new file mode 100644 index 00000000..81689bb6 --- /dev/null +++ b/app/Http/Middleware/RateLimitMiddleware.php @@ -0,0 +1,106 @@ +endpoint_repository = $endpoint_repository; + $this->cache_service = $cache_service; + } + + /** + * Handle an incoming request. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * @return mixed + */ + public function handle($request, Closure $next) + { + $response = $next($request); + // if response was not changed then short circuit ... + if ($response->getStatusCode() === 304) + { + return $response; + } + + $url = $request->getRequestUri(); + + try + { + $route = RequestUtils::getCurrentRoutePath($request); + $method = $request->getMethod(); + $endpoint = $this->endpoint_repository->getApiEndpointByUrlAndMethod($route, $method); + + if (!is_null($endpoint->rate_limit) && ($requestsPerHour = (int)$endpoint->rate_limit) > 0) + { + //do rate limit checking + $key = sprintf('rate.limit.%s_%s_%s', $url, $method, $request->getClientIp()); + // Add if doesn't exist + // Remember for 1 hour + $this->cache_service->addSingleValue($key, 0, 3600); + // Add to count + $count = $this->cache_service->incCounter($key); + if ( $count > $requestsPerHour ) + { + // Short-circuit response - we're ignoring + $response = Response::json(array( + 'message' => "You have triggered an abuse detection mechanism and have been temporarily blocked. + Please retry your request again later."), 403); + $ttl = (int) $this->cache_service->ttl($key); + $response->headers->set('X-RateLimit-Reset', $ttl, false); + } + $response->headers->set('X-Ratelimit-Limit', $requestsPerHour, false); + $remaining = $requestsPerHour-(int)$count; + if ($remaining < 0) + { + $remaining = 0; + } + $response->headers->set('X-Ratelimit-Remaining', $remaining, false); + } + } + catch (Exception $ex) + { + Log::error($ex); + } + return $response; + } +} \ No newline at end of file diff --git a/app/Http/Middleware/RedirectIfAuthenticated.php b/app/Http/Middleware/RedirectIfAuthenticated.php new file mode 100644 index 00000000..dd5a8672 --- /dev/null +++ b/app/Http/Middleware/RedirectIfAuthenticated.php @@ -0,0 +1,44 @@ +auth = $auth; + } + + /** + * Handle an incoming request. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * @return mixed + */ + public function handle($request, Closure $next) + { + if ($this->auth->check()) + { + return new RedirectResponse(url('/home')); + } + + return $next($request); + } + +} diff --git a/app/Http/Middleware/SecurityHTTPHeadersWriterMiddleware.php b/app/Http/Middleware/SecurityHTTPHeadersWriterMiddleware.php new file mode 100644 index 00000000..257f996f --- /dev/null +++ b/app/Http/Middleware/SecurityHTTPHeadersWriterMiddleware.php @@ -0,0 +1,50 @@ +headers->set('X-content-type-options', 'nosniff'); + $response->headers->set('X-xss-protection', '1; mode=block'); + // http://tools.ietf.org/html/rfc6797 + /** + * The HSTS header field below stipulates that the HSTS Policy is to + * remain in effect for one year (there are approximately 31536000 + * seconds in a year) + * applies to the domain of the issuing HSTS Host and all of its + * subdomains: + */ + $response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); + return $response; + } +} \ No newline at end of file diff --git a/app/Http/Middleware/VerifyCsrfToken.php b/app/Http/Middleware/VerifyCsrfToken.php new file mode 100644 index 00000000..1977cd7e --- /dev/null +++ b/app/Http/Middleware/VerifyCsrfToken.php @@ -0,0 +1,20 @@ + 'api/v1', + 'before' => ['ssl', 'oauth2.enabled'], + 'after' => '', + 'middleware' => ['oauth2.protected', 'rate.limit','etags']), function () { + + Route::group(array('prefix' => 'marketplace'), function () { + + Route::group(array('prefix' => 'public-clouds'), function () { + Route::get('', 'OAuth2PublicCloudApiController@getClouds'); + Route::get('/{id}', 'OAuth2PublicCloudApiController@getCloud'); + Route::get('/{id}/data-centers', 'OAuth2PublicCloudApiController@getCloudDataCenters'); + }); + + Route::group(array('prefix' => 'private-clouds'), function () { + Route::get('', 'OAuth2PrivateCloudApiController@getClouds'); + Route::get('/{id}', 'OAuth2PrivateCloudApiController@getCloud'); + Route::get('/{id}/data-centers', 'OAuth2PrivateCloudApiController@getCloudDataCenters'); + }); + + Route::group(array('prefix' => 'consultants'), function () { + Route::get('', 'OAuth2ConsultantsApiController@getConsultants'); + Route::get('/{id}', 'OAuth2ConsultantsApiController@getConsultant'); + Route::get('/{id}/offices', 'OAuth2ConsultantsApiController@getOffices'); + }); + + }); + }); \ No newline at end of file diff --git a/app/Libs/oauth2/BearerAccessTokenAuthorizationHeaderParser.php b/app/Libs/oauth2/BearerAccessTokenAuthorizationHeaderParser.php new file mode 100644 index 00000000..f214185b --- /dev/null +++ b/app/Libs/oauth2/BearerAccessTokenAuthorizationHeaderParser.php @@ -0,0 +1,77 @@ +container = $values; + } + + /** + * arrayaccess methods + * */ + public function offsetSet($offset, $value) + { + if (is_null($offset)) + { + $this->container[] = $value; + } + else + { + $this->container[$offset] = $value; + } + } + + public function offsetExists($offset) + { + return isset($this->container[$offset]); + } + + public function offsetUnset($offset) + { + unset($this->container[$offset]); + } + + public function offsetGet($offset) + { + return isset($this->container[$offset]) ? $this->container[$offset] : null; + } +} \ No newline at end of file diff --git a/app/Libs/oauth2/HttpResponse.php b/app/Libs/oauth2/HttpResponse.php new file mode 100644 index 00000000..63feea5f --- /dev/null +++ b/app/Libs/oauth2/HttpResponse.php @@ -0,0 +1,52 @@ +http_code = $http_code; + $this->content_type = $content_type; + } + + abstract public function getContent(); + + public function getHttpCode() + { + return $this->http_code; + } + + protected function setHttpCode($http_code) + { + $this->http_code = $http_code; + } + + public function getContentType() + { + return $this->content_type; + } + + abstract public function getType(); + + public function addParam($name, $value) + { + $this[$name] = $value; + } +} \ No newline at end of file diff --git a/app/Libs/oauth2/InvalidGrantTypeException.php b/app/Libs/oauth2/InvalidGrantTypeException.php new file mode 100644 index 00000000..384f9826 --- /dev/null +++ b/app/Libs/oauth2/InvalidGrantTypeException.php @@ -0,0 +1,28 @@ +container); + return $json_encoded_format; + } + + public function getType() + { + return self::OAuth2DirectResponse; + } +} \ No newline at end of file diff --git a/app/Libs/oauth2/OAuth2InvalidIntrospectionResponse.php b/app/Libs/oauth2/OAuth2InvalidIntrospectionResponse.php new file mode 100644 index 00000000..db7c8a56 --- /dev/null +++ b/app/Libs/oauth2/OAuth2InvalidIntrospectionResponse.php @@ -0,0 +1,24 @@ + self::OAuth2Protocol_ResponseType_Code, + self::OAuth2Protocol_ResponseType_Token => self::OAuth2Protocol_ResponseType_Token + ); + public static $protocol_definition = array( + self::OAuth2Protocol_ResponseType => self::OAuth2Protocol_ResponseType, + self::OAuth2Protocol_ClientId => self::OAuth2Protocol_ClientId, + self::OAuth2Protocol_RedirectUri => self::OAuth2Protocol_RedirectUri, + self::OAuth2Protocol_Scope => self::OAuth2Protocol_Scope, + self::OAuth2Protocol_State => self::OAuth2Protocol_State + ); + +} \ No newline at end of file diff --git a/app/Libs/oauth2/OAuth2ResourceServerException.php b/app/Libs/oauth2/OAuth2ResourceServerException.php new file mode 100644 index 00000000..25f0b168 --- /dev/null +++ b/app/Libs/oauth2/OAuth2ResourceServerException.php @@ -0,0 +1,58 @@ +http_code = $http_code; + $this->error = $error; + $this->error_description = $error_description; + $this->scope = $scope; + $message = "Resource Server Exception : " . sprintf('http code : %s - error : %s - error description: %s', $http_code, $error, $error_description); + parent::__construct($message, 0, null); + } + + public function getError() + { + return $this->error; + } + + public function getErrorDescription() + { + return $this->error_description; + } + + public function getScope() + { + return $this->scope; + } + + public function getHttpCode() + { + return $this->http_code; + } +} \ No newline at end of file diff --git a/app/Libs/oauth2/OAuth2Response.php b/app/Libs/oauth2/OAuth2Response.php new file mode 100644 index 00000000..0d5f7eb9 --- /dev/null +++ b/app/Libs/oauth2/OAuth2Response.php @@ -0,0 +1,18 @@ +realm = $realm; + $this->error = $error; + $this->error_description = $error_description; + $this->scope = $scope; + $this->http_error = $http_error; + } + + public function getWWWAuthenticateHeaderValue() + { + $value=sprintf('Bearer realm="%s"', $this->realm); + $value=$value.sprintf(', error="%s"', $this->error); + $value=$value.sprintf(', error_description="%s"', $this->error_description); + if (!is_null($this->scope)) + { + $value=$value.sprintf(', scope="%s"', $this->scope); + } + return $value; + } + + + public function getContent() + { + $content = array( + 'error' => $this->error, + 'error_description' => $this->error_description + ); + if (!is_null($this->scope)) + { + $content['scope'] = $this->scope; + } + return $content; + } + + public function getType() + { + return null; + } +} \ No newline at end of file diff --git a/app/Libs/utils/ConfigurationException.php b/app/Libs/utils/ConfigurationException.php new file mode 100644 index 00000000..0eeb00a7 --- /dev/null +++ b/app/Libs/utils/ConfigurationException.php @@ -0,0 +1,29 @@ +getRoutes(); + $route = $routes->match($request); + if (!is_null($route)) + { + $route = $route->getPath(); + if (strpos($route, '/') != 0) + { + $route = '/' . $route; + } + return $route; + } + } + catch (\Exception $ex) + { + Log::error($ex); + } + return false; + } + +} \ No newline at end of file diff --git a/app/Models/Marketplace/CompanyService.php b/app/Models/Marketplace/CompanyService.php new file mode 100644 index 00000000..114bf2c3 --- /dev/null +++ b/app/Models/Marketplace/CompanyService.php @@ -0,0 +1,38 @@ +ID; + } +} \ No newline at end of file diff --git a/app/Models/Marketplace/Consultant.php b/app/Models/Marketplace/Consultant.php new file mode 100644 index 00000000..ef1beaa4 --- /dev/null +++ b/app/Models/Marketplace/Consultant.php @@ -0,0 +1,28 @@ +hasMany('models\marketplace\Office', 'ConsultantID', 'ID')->get(); + } +} \ No newline at end of file diff --git a/app/Models/Marketplace/DataCenterLocation.php b/app/Models/Marketplace/DataCenterLocation.php new file mode 100644 index 00000000..6e28b528 --- /dev/null +++ b/app/Models/Marketplace/DataCenterLocation.php @@ -0,0 +1,37 @@ +belongsTo('models\marketplace\DataCenterRegion', 'DataCenterRegionID'); + } +} \ No newline at end of file diff --git a/app/Models/Marketplace/DataCenterRegion.php b/app/Models/Marketplace/DataCenterRegion.php new file mode 100644 index 00000000..dbb74aa0 --- /dev/null +++ b/app/Models/Marketplace/DataCenterRegion.php @@ -0,0 +1,38 @@ +hasMany('models\marketplace\DataCenterLocation', 'DataCenterRegionID', 'ID')->get(); + } + +} \ No newline at end of file diff --git a/app/Models/Marketplace/ICloudService.php b/app/Models/Marketplace/ICloudService.php new file mode 100644 index 00000000..42931418 --- /dev/null +++ b/app/Models/Marketplace/ICloudService.php @@ -0,0 +1,26 @@ +belongsTo('models\marketplace\Consultant', 'ConsultantID'); + } +} \ No newline at end of file diff --git a/app/Models/Marketplace/PrivateCloudService.php b/app/Models/Marketplace/PrivateCloudService.php new file mode 100644 index 00000000..50865f11 --- /dev/null +++ b/app/Models/Marketplace/PrivateCloudService.php @@ -0,0 +1,31 @@ +hasMany('models\marketplace\DataCenterRegion', 'CloudServiceID', 'ID')->get(); + } + +} \ No newline at end of file diff --git a/app/Models/Marketplace/PublicCloudService.php b/app/Models/Marketplace/PublicCloudService.php new file mode 100644 index 00000000..efb0543c --- /dev/null +++ b/app/Models/Marketplace/PublicCloudService.php @@ -0,0 +1,30 @@ +hasMany('models\marketplace\DataCenterRegion', 'CloudServiceID', 'ID')->get(); + } +} \ No newline at end of file diff --git a/app/Models/ResourceServer/AccessTokenService.php b/app/Models/ResourceServer/AccessTokenService.php new file mode 100644 index 00000000..81131357 --- /dev/null +++ b/app/Models/ResourceServer/AccessTokenService.php @@ -0,0 +1,154 @@ +cache_service = $cache_service; + } + + /** + * @param string $token_value + * @return AccessToken + * @throws \Exception + */ + public function get($token_value) + { + $token = null; + + + $token_info = $this->cache_service->getHash(md5($token_value), array( + 'access_token', + 'scope', + 'client_id', + 'audience', + 'user_id', + 'expires_in', + 'application_type', + 'allowed_return_uris', + 'allowed_origins')); + + if (count($token_info) === 0) + { + $token_info = $this->makeRemoteCall($token_value); + $this->cache_service->storeHash(md5($token_value), $token_info, (int)$token_info['expires_in']); + } + else + { + $token_info['expires_in'] = $this->cache_service->ttl(md5($token_value)); + } + + $token = AccessToken::createFromParams( + $token_info['access_token'], + $token_info['scope'], + $token_info['client_id'], + $token_info['audience'], + $token_info['user_id'], + (int)$token_info['expires_in'], + $token_info['application_type'], + isset($token_info['allowed_return_uris']) ? $token_info['allowed_return_uris'] : null, + isset($token_info['allowed_origins']) ? $token_info['allowed_origins'] : null + ); + + return $token; + } + + /** + * @param $token_value + * @return mixed + * @throws ConfigurationException + * @throws InvalidGrantTypeException + * @throws OAuth2InvalidIntrospectionResponse + */ + private function makeRemoteCall($token_value) + { + + try + { + $client = new Client([ + 'defaults' => [ + 'timeout' => Config::get('curl.timeout', 60), + 'allow_redirects' => Config::get('curl.allow_redirects', false), + 'verify' => Config::get('curl.verify_ssl_cert', true) + ] + ]); + + $client_id = Config::get('app.openstackid_client_id', ''); + $client_secret = Config::get('app.openstackid_client_secret', ''); + $auth_server_url = Config::get('app.openstackid_base_url', ''); + + if (empty($client_id)) + { + throw new ConfigurationException('app.openstackid_client_id param is missing!'); + } + + if (empty($client_secret)) + { + throw new ConfigurationException('app.openstackid_client_secret param is missing!'); + } + + if (empty($auth_server_url)) + { + throw new ConfigurationException('app.openstackid_base_url param is missing!'); + } + + $response = $client->post( + $auth_server_url . '/oauth2/token/introspection', + [ + 'query' => ['token' => $token_value], + 'headers' => ['Authorization' => " Basic " . base64_encode($client_id . ':' . $client_secret)] + ] + ); + + $token_info = $response->json(); + + return $token_info; + + } + catch (RequestException $ex) + { + $response = $ex->getResponse(); + $body = $response->json(); + $code = $response->getStatusCode(); + if ($code === 400) + { + throw new InvalidGrantTypeException($body['error']); + } + throw new OAuth2InvalidIntrospectionResponse(sprintf('http code %s', $ex->getCode())); + } + } +} \ No newline at end of file diff --git a/app/Models/ResourceServer/Api.php b/app/Models/ResourceServer/Api.php new file mode 100644 index 00000000..2b6092ba --- /dev/null +++ b/app/Models/ResourceServer/Api.php @@ -0,0 +1,100 @@ +hasMany('models\resource_server\ApiScope', 'api_id'); + } + + /** + * @return IApiEndpoint[] + */ + public function endpoints() + { + return $this->hasMany('models\resource_server\ApiEndpoint', 'api_id'); + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * @return string + */ + public function getScope() + { + $scope = ''; + foreach ($this->scopes()->get() as $s) + { + if (!$s->active) + { + continue; + } + $scope = $scope .$s->name.' '; + } + $scope = trim($scope); + return $scope; + } + + /** + * @return bool + */ + public function isActive() + { + return $this->active; + } + + public function setName($name) + { + $this->name = $name; + } + + public function setDescription($description) + { + $this->description = $description; + } + + public function setStatus($active) + { + $this->active = $active; + } +} \ No newline at end of file diff --git a/app/Models/ResourceServer/ApiEndpoint.php b/app/Models/ResourceServer/ApiEndpoint.php new file mode 100644 index 00000000..9706f9a3 --- /dev/null +++ b/app/Models/ResourceServer/ApiEndpoint.php @@ -0,0 +1,134 @@ +belongsTo('models\resource_server\Api', 'api_id'); + } + + /** + * @return IApiScope[] + */ + public function scopes() + { + return $this->belongsToMany('models\resource_server\ApiScope', 'endpoint_api_scopes', 'api_endpoint_id', 'scope_id'); + } + + public function getRoute() + { + return $this->route; + } + + public function getHttpMethod() + { + return $this->http_method; + } + + public function setRoute($route) + { + $this->route = $route; + } + + public function setHttpMethod($http_method) + { + $this->http_method = $http_method; + } + + /** + * @return string + */ + public function getScope() + { + $scope = ''; + foreach ($this->scopes()->get() as $s) + { + if (!$s->active) + { + continue; + } + $scope = $scope .$s->name.' '; + } + $scope = trim($scope); + return $scope; + } + + public function isActive() + { + return $this->active; + } + + /** + * @param bool $active + */ + public function setStatus($active) + { + $this->active = $active; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @param string $name + */ + public function setName($name) + { + $this->name= $name; + } + + /** + * @return bool + */ + public function supportCORS() + { + return $this->allow_cors; + } + + /** + * @return bool + */ + public function supportCredentials() + { + return (bool)$this->allow_credentials; + } +} \ No newline at end of file diff --git a/app/Models/ResourceServer/ApiScope.php b/app/Models/ResourceServer/ApiScope.php new file mode 100644 index 00000000..bf91ea27 --- /dev/null +++ b/app/Models/ResourceServer/ApiScope.php @@ -0,0 +1,57 @@ +belongsTo('models\resource_server\Api', 'api_id'); + } + + public function getShortDescription() + { + return $this->short_description; + } + + public function getName() + { + return $this->name; + } + + public function getDescription() + { + return $this->description; + } + + public function isActive() + { + return $this->active; + } +} \ No newline at end of file diff --git a/app/Models/ResourceServer/IAccessTokenService.php b/app/Models/ResourceServer/IAccessTokenService.php new file mode 100644 index 00000000..4befc6ca --- /dev/null +++ b/app/Models/ResourceServer/IAccessTokenService.php @@ -0,0 +1,30 @@ +where($filter['name'], $filter['op'], $filter['value']); + } + return $query; + } + + public function __construct($attributes = array()) + { + parent::__construct($attributes); + $this->class = new ReflectionClass(get_class($this)); + if ($this->useSti()) + { + $this->setAttribute($this->stiClassField, $this->class->getName()); + } + } + + private function useSti() + { + return ($this->stiClassField && $this->stiBaseClass); + } + + public function newQuery($excludeDeleted = true) + { + $builder = parent::newQuery($excludeDeleted); + // If I am using STI, and I am not the base class, + // then filter on the class name. + if ($this->useSti() && get_class(new $this->stiBaseClass) !== get_class($this)) + { + $builder->where($this->stiClassField, "=", $this->class->getShortName()); + } + return $builder; + } + + public function newFromBuilder($attributes = array(), $connection = null) + { + if ($this->useSti() && $attributes->{$this->stiClassField}) + { + $class = $this->class->getName(); + $instance = new $class; + $instance->exists = true; + $instance->setRawAttributes((array) $attributes, true); + return $instance; + } + else + { + return parent::newFromBuilder($attributes, $connection); + } + } +} \ No newline at end of file diff --git a/app/Models/Utils/IBaseRepository.php b/app/Models/Utils/IBaseRepository.php new file mode 100644 index 00000000..9943057b --- /dev/null +++ b/app/Models/Utils/IBaseRepository.php @@ -0,0 +1,24 @@ +value = $value; + $instance->scope = $scope; + $instance->client_id = $client_id; + $instance->user_id = $user_id; + $instance->auth_code = null; + $instance->audience = $audience; + $instance->refresh_token = null; + $instance->lifetime = intval($lifetime); + $instance->is_hashed = false; + $instance->allowed_return_uris = $allowed_return_uris; + $instance->application_type = $application_type; + $instance->allowed_origins = $allowed_origins; + return $instance; + } + + public function getAuthCode() + { + return $this->auth_code; + } + + public function getRefreshToken() + { + return $this->refresh_token; + } + + public function getApplicationType() + { + return $this->application_type; + } + + public function getAllowedOrigins() + { + return $this->allowed_origins; + } + + public function getAllowedReturnUris() + { + return $this->allowed_return_uris; + } + + public function toJSON() + { + return '{}'; + } + + public function fromJSON($json) + { + + } +} \ No newline at end of file diff --git a/app/Models/oauth2/IResourceServerContext.php b/app/Models/oauth2/IResourceServerContext.php new file mode 100644 index 00000000..69672dde --- /dev/null +++ b/app/Models/oauth2/IResourceServerContext.php @@ -0,0 +1,58 @@ +auth_context['scope'])? explode(' ', $this->auth_context['scope']):array(); + } + + /** + * @return null|string + */ + public function getCurrentAccessToken() + { + return isset($this->auth_context['access_token'])?$this->auth_context['access_token']:null; + } + + + /** + * @return null|string + */ + public function getCurrentAccessTokenLifetime() + { + return isset($this->auth_context['expires_in'])?$this->auth_context['expires_in']:null; + } + + /** + * @return null + */ + public function getCurrentClientId() + { + 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 array $auth_context + * @return void + */ + public function setAuthorizationContext(array $auth_context) + { + $this->auth_context = $auth_context; + } +} \ No newline at end of file diff --git a/app/Models/oauth2/Token.php b/app/Models/oauth2/Token.php new file mode 100644 index 00000000..5b233f95 --- /dev/null +++ b/app/Models/oauth2/Token.php @@ -0,0 +1,90 @@ +len = $len; + $this->is_hashed = false; + } + + public function getValue() + { + return $this->value; + } + + public function getLifetime() + { + return intval($this->lifetime); + } + + public function getScope() + { + return $this->scope; + } + + public function getClientId() + { + return $this->client_id; + } + + public function getAudience() + { + return $this->audience; + } + + public function getFromIp() + { + return $this->from_ip; + } + + public function getUserId() + { + return $this->user_id; + } + + public function isHashed() + { + return $this->is_hashed; + } + + public abstract function toJSON(); + + + public abstract function fromJSON($json); +} \ No newline at end of file diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php new file mode 100644 index 00000000..1c786225 --- /dev/null +++ b/app/Providers/AppServiceProvider.php @@ -0,0 +1,47 @@ +pushHandler($handler); + } + + } + + /** + * Register any application services. + * + * @return void + */ + public function register() + { + App::singleton('models\\oauth2\\IResourceServerContext', 'models\\oauth2\\ResourceServerContext'); + App::singleton('models\resource_server\\IAccessTokenService', 'models\resource_server\\AccessTokenService'); + App::singleton('models\\resource_server\\IApi', 'models\\resource_server\\Api'); + App::singleton('models\\resource_server\\IApiEndpoint', 'models\\resource_server\\ApiEndpoint'); + App::singleton('models\\resource_server\\IApiScope', 'models\\resource_server\\ApiScope'); + } + +} \ No newline at end of file diff --git a/app/Providers/BusServiceProvider.php b/app/Providers/BusServiceProvider.php new file mode 100644 index 00000000..17778ab6 --- /dev/null +++ b/app/Providers/BusServiceProvider.php @@ -0,0 +1,34 @@ +mapUsing(function($command) + { + return Dispatcher::simpleMapping( + $command, 'App\Commands', 'App\Handlers\Commands' + ); + }); + } + + /** + * Register any application services. + * + * @return void + */ + public function register() + { + // + } + +} \ No newline at end of file diff --git a/app/Providers/ConfigServiceProvider.php b/app/Providers/ConfigServiceProvider.php new file mode 100644 index 00000000..dc9a1269 --- /dev/null +++ b/app/Providers/ConfigServiceProvider.php @@ -0,0 +1,23 @@ + [ + 'EventListener', + ], + ]; + + /** + * Register any other events for your application. + * + * @param \Illuminate\Contracts\Events\Dispatcher $events + * @return void + */ + public function boot(DispatcherContract $events) + { + parent::boot($events); + + // + } + +} \ No newline at end of file diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php new file mode 100644 index 00000000..40477cbe --- /dev/null +++ b/app/Providers/RouteServiceProvider.php @@ -0,0 +1,62 @@ +group(['namespace' => $this->namespace], function ($router) { + require app_path('Http/routes.php'); + }); + } + +} \ No newline at end of file diff --git a/app/Repositories/RepositoriesProvider.php b/app/Repositories/RepositoriesProvider.php new file mode 100644 index 00000000..f73594b8 --- /dev/null +++ b/app/Repositories/RepositoriesProvider.php @@ -0,0 +1,49 @@ +entity->find($id); + } + + /** + * @param int $page + * @param int $per_page + * @param string $status + * @param string $order_by + * @param string $order_dir + * @return IEntity[] + */ + public function getAll( + $page = 1, + $per_page = 1000, + $status = ICompanyServiceRepository::Status_All, + $order_by = ICompanyServiceRepository::Order_date, + $order_dir = 'asc' + ) { + $fields = array('*'); + $filters = array(); + switch($status) + { + case ICompanyServiceRepository::Status_active: + array_push( + $filters, + array( + 'name'=>'Active', + 'op' => '=', + 'value'=> true + ) + ); + break; + case ICompanyServiceRepository::Status_non_active: + array_push( + $filters, + array( + 'name'=>'Active', + 'op' => '=', + 'value'=> false + ) + ); + break; + } + + $query = $this->entity->Filter($filters); + + switch($order_by) + { + case ICompanyServiceRepository::Order_date: + $query = $query->orderBy('Created', $order_dir); + break; + case ICompanyServiceRepository::Order_name: + $query = $query->orderBy('Name', $order_dir); + break; + } + + return $query->paginate($per_page, $fields)->toArray(); + } +} \ No newline at end of file diff --git a/app/Repositories/marketplace/EloquentConsultantRepository.php b/app/Repositories/marketplace/EloquentConsultantRepository.php new file mode 100644 index 00000000..8f3c188c --- /dev/null +++ b/app/Repositories/marketplace/EloquentConsultantRepository.php @@ -0,0 +1,32 @@ +entity = $consultant; + } +} \ No newline at end of file diff --git a/app/Repositories/marketplace/EloquentPrivateCloudServiceRepository.php b/app/Repositories/marketplace/EloquentPrivateCloudServiceRepository.php new file mode 100644 index 00000000..3d4b2937 --- /dev/null +++ b/app/Repositories/marketplace/EloquentPrivateCloudServiceRepository.php @@ -0,0 +1,35 @@ +entity = $private_cloud; + } + +} \ No newline at end of file diff --git a/app/Repositories/marketplace/EloquentPublicCloudServiceRepository.php b/app/Repositories/marketplace/EloquentPublicCloudServiceRepository.php new file mode 100644 index 00000000..9620d7eb --- /dev/null +++ b/app/Repositories/marketplace/EloquentPublicCloudServiceRepository.php @@ -0,0 +1,34 @@ +entity = $public_cloud; + } +} \ No newline at end of file diff --git a/app/Repositories/resource_server/EloquentApiEndpointRepository.php b/app/Repositories/resource_server/EloquentApiEndpointRepository.php new file mode 100644 index 00000000..9cff7151 --- /dev/null +++ b/app/Repositories/resource_server/EloquentApiEndpointRepository.php @@ -0,0 +1,67 @@ +entity = $endpoint; + } + /** + * @param string $url + * @param string $http_method + * @return IApiEndpoint + */ + public function getApiEndpointByUrlAndMethod($url, $http_method) + { + return $this->entity->Filter(array( array( + 'name'=>'route', + 'op' => '=', + 'value'=> $url + ), array( + 'name'=>'http_method', + 'op' => '=', + 'value'=> $http_method + )))->firstOrFail(); + } + + /** + * @param int $id + * @return IEntity + */ + public function getById($id) + { + return $this->entity->find($id); + } +} \ No newline at end of file diff --git a/app/Services/ServicesProvider.php b/app/Services/ServicesProvider.php new file mode 100644 index 00000000..0e2da689 --- /dev/null +++ b/app/Services/ServicesProvider.php @@ -0,0 +1,34 @@ +redis = Redis::connection(); + } + + + public function boot() + { + if (is_null($this->redis)) + { + $this->redis = Redis::connection(); + } + } + /** + * @param $key + * @return mixed + */ + public function delete($key) + { + $res = 0; + if ($this->redis->exists($key)) + { + $res = $this->redis->del($key); + } + return $res; + } + + public function deleteArray(array $keys) + { + if (count($keys)>0) + { + $this->redis->del($keys); + } + } + + /** + * @param $key + * @return bool + */ + public function exists($key) + { + $res = $this->redis->exists($key); + return $res>0; + } + + /** + * @param $name + * @param array $values + * @return mixed + */ + public function getHash($name, array $values) + { + $res = array(); + if ($this->redis->exists($name)) + { + $cache_values = $this->redis->hmget($name, $values); + for ($i=0; $iredis->exists($name)) + { + $this->redis->hmset($name, $values); + $res = true; + //sets expiration time + if ($ttl>0) + { + $this->redis->expire($name, $ttl); + } + } + return $res; + } + + public function incCounter($counter_name, $ttl = 0) + { + if ($this->redis->setnx($counter_name, 1)) + { + $this->redis->expire($counter_name, $ttl); + return 1; + } + else + { + return (int)$this->redis->incr($counter_name); + } + } + + public function incCounterIfExists($counter_name) + { + $res = false; + if ($this->redis->exists($counter_name)) + { + $this->redis->incr($counter_name); + $res = true; + } + return $res; + } + + public function addMemberSet($set_name, $member) + { + return $this->redis->sadd($set_name, $member); + } + + public function deleteMemberSet($set_name, $member) + { + return $this->redis->srem($set_name, $member); + } + + public function getSet($set_name) + { + return $this->redis->smembers($set_name); + } + + public function getSingleValue($key) + { + return $this->redis->get($key); + } + + public function setSingleValue($key, $value, $ttl = 0) + { + if ($ttl>0) + { + return $this->redis->setex($key, $ttl, $value); + } + else + { + return $this->redis->set($key, $value); + } + } + + public function addSingleValue($key, $value, $ttl = 0) + { + $res = $this->redis->setnx($key, $value); + if ($res && $ttl>0) + { + $this->redis->expire($key, $ttl); + } + return $res; + } + + public function setKeyExpiration($key, $ttl) + { + $this->redis->expire($key, intval($ttl)); + } + + /**Returns the remaining time to live of a key that has a timeout. + * @param string $key + * @return int + */ + public function ttl($key) + { + return (int)$this->redis->ttl($key); + } +} \ No newline at end of file diff --git a/artisan b/artisan new file mode 100755 index 00000000..eb5e2bb6 --- /dev/null +++ b/artisan @@ -0,0 +1,51 @@ +#!/usr/bin/env php +make('Illuminate\Contracts\Console\Kernel'); + +$status = $kernel->handle( + $input = new Symfony\Component\Console\Input\ArgvInput, + new Symfony\Component\Console\Output\ConsoleOutput +); + +/* +|-------------------------------------------------------------------------- +| Shutdown The Application +|-------------------------------------------------------------------------- +| +| Once Artisan has finished running. We will fire off the shutdown events +| so that any final work may be done by the application before we shut +| down the process. This is the last thing to happen to the request. +| +*/ + +$kernel->terminate($input, $status); + +exit($status); diff --git a/bootstrap/app.php b/bootstrap/app.php new file mode 100644 index 00000000..f9e94b4b --- /dev/null +++ b/bootstrap/app.php @@ -0,0 +1,57 @@ +singleton( + 'Illuminate\Contracts\Http\Kernel', + 'App\Http\Kernel' +); + +$app->singleton( + 'Illuminate\Contracts\Console\Kernel', + 'App\Console\Kernel' +); + +$app->singleton( + 'Illuminate\Contracts\Debug\ExceptionHandler', + 'App\Exceptions\Handler' +); + + +/* +|-------------------------------------------------------------------------- +| Return The Application +|-------------------------------------------------------------------------- +| +| This script returns the application instance. The instance is given to +| the calling script so we can separate the building of the instances +| from the actual running of the application and sending responses. +| +*/ + + +return $app; \ No newline at end of file diff --git a/bootstrap/autoload.php b/bootstrap/autoload.php new file mode 100644 index 00000000..17718f5d --- /dev/null +++ b/bootstrap/autoload.php @@ -0,0 +1,35 @@ +=5.4.0", + "guzzlehttp/guzzle": "5.2.0" + }, + "require-dev": { + "phpunit/phpunit": "4.6.6", + "phpspec/phpspec": "~2.1", + "mockery/mockery": "0.9.4", + "squizlabs/php_codesniffer": "2.*", + "pragmarx/laravelcs": "*", + "glenscott/url-normalizer" : "1.4.0" + }, + "autoload": { + "classmap": [ + "database", + "app" + ], + "psr-4": { + "App\\": "app/" + } + }, + "autoload-dev": { + "classmap": [ + "tests" + ] + }, + "scripts": { + "post-install-cmd": [ + "php artisan clear-compiled", + "php artisan optimize" + ], + "post-update-cmd": [ + "php artisan clear-compiled", + "php artisan optimize" + ], + "post-create-project-cmd": [ + "php -r \"copy('.env.example', '.env');\"", + "php artisan key:generate" + ] + }, + "config": { + "preferred-install": "dist" + } +} diff --git a/config/app.php b/config/app.php new file mode 100644 index 00000000..876c189f --- /dev/null +++ b/config/app.php @@ -0,0 +1,202 @@ + env('APP_OAUTH_2_0_CLIENT_ID'), + 'openstackid_client_secret' => env('APP_OAUTH_2_0_CLIENT_SECRET'), + 'openstackid_base_url' => env('APP_OAUTH_2_0_AUTH_SERVER_BASE_URL'), + /* + |-------------------------------------------------------------------------- + | Application Debug Mode + |-------------------------------------------------------------------------- + | + | When your application is in debug mode, detailed error messages with + | stack traces will be shown on every error that occurs within your + | application. If disabled, a simple generic error page is shown. + | + */ + + 'debug' => env('APP_DEBUG', false), + + /* + |-------------------------------------------------------------------------- + | Application URL + |-------------------------------------------------------------------------- + | + | This URL is used by the console to properly generate URLs when using + | the Artisan command line tool. You should set this to the root of + | your application so that it is used when running Artisan tasks. + | + */ + + 'url' => env('APP_URL', 'http://localhost'), + + /* + |-------------------------------------------------------------------------- + | Application Timezone + |-------------------------------------------------------------------------- + | + | Here you may specify the default timezone for your application, which + | will be used by the PHP date and date-time functions. We have gone + | ahead and set this to a sensible default for you out of the box. + | + */ + + 'timezone' => 'UTC', + + /* + |-------------------------------------------------------------------------- + | Application Locale Configuration + |-------------------------------------------------------------------------- + | + | The application locale determines the default locale that will be used + | by the translation service provider. You are free to set this value + | to any of the locales which will be supported by the application. + | + */ + + 'locale' => 'en', + + /* + |-------------------------------------------------------------------------- + | Application Fallback Locale + |-------------------------------------------------------------------------- + | + | The fallback locale determines the locale to use when the current one + | is not available. You may change the value to correspond to any of + | the language folders that are provided through your application. + | + */ + + 'fallback_locale' => 'en', + + /* + |-------------------------------------------------------------------------- + | Encryption Key + |-------------------------------------------------------------------------- + | + | This key is used by the Illuminate encrypter service and should be set + | to a random, 32 character string, otherwise these encrypted strings + | will not be safe. Please do this before deploying an application! + | + */ + + 'key' => env('APP_KEY', 'SomeRandomString'), + + 'cipher' => MCRYPT_RIJNDAEL_128, + + /* + |-------------------------------------------------------------------------- + | Logging Configuration + |-------------------------------------------------------------------------- + | + | Here you may configure the log settings for your application. Out of + | the box, Laravel uses the Monolog PHP logging library. This gives + | you a variety of powerful log handlers / formatters to utilize. + | + | Available Settings: "single", "daily", "syslog", "errorlog" + | + */ + + 'log' => 'daily', + + /* + |-------------------------------------------------------------------------- + | Autoloaded Service Providers + |-------------------------------------------------------------------------- + | + | The service providers listed here will be automatically loaded on the + | request to your application. Feel free to add your own services to + | this array to grant expanded functionality to your applications. + | + */ + + 'providers' => [ + + /* + * Laravel Framework Service Providers... + */ + 'Illuminate\Foundation\Providers\ArtisanServiceProvider', + 'Illuminate\Auth\AuthServiceProvider', + 'Illuminate\Bus\BusServiceProvider', + 'Illuminate\Cache\CacheServiceProvider', + 'Illuminate\Foundation\Providers\ConsoleSupportServiceProvider', + 'Illuminate\Routing\ControllerServiceProvider', + 'Illuminate\Cookie\CookieServiceProvider', + 'Illuminate\Database\DatabaseServiceProvider', + 'Illuminate\Encryption\EncryptionServiceProvider', + 'Illuminate\Filesystem\FilesystemServiceProvider', + 'Illuminate\Foundation\Providers\FoundationServiceProvider', + 'Illuminate\Hashing\HashServiceProvider', + 'Illuminate\Mail\MailServiceProvider', + 'Illuminate\Pagination\PaginationServiceProvider', + 'Illuminate\Pipeline\PipelineServiceProvider', + 'Illuminate\Queue\QueueServiceProvider', + 'Illuminate\Redis\RedisServiceProvider', + 'Illuminate\Auth\Passwords\PasswordResetServiceProvider', + 'Illuminate\Session\SessionServiceProvider', + 'Illuminate\Translation\TranslationServiceProvider', + 'Illuminate\Validation\ValidationServiceProvider', + 'Illuminate\View\ViewServiceProvider', + + /* + * Application Service Providers... + */ + 'App\Providers\AppServiceProvider', + 'App\Providers\BusServiceProvider', + 'App\Providers\ConfigServiceProvider', + 'App\Providers\EventServiceProvider', + 'App\Providers\RouteServiceProvider', + 'repositories\RepositoriesProvider', + 'services\ServicesProvider', + ], + + /* + |-------------------------------------------------------------------------- + | Class Aliases + |-------------------------------------------------------------------------- + | + | This array of class aliases will be registered when this application + | is started. However, feel free to register as many as you wish as + | the aliases are "lazy" loaded so they don't hinder performance. + | + */ + + 'aliases' => [ + + 'App' => 'Illuminate\Support\Facades\App', + 'Artisan' => 'Illuminate\Support\Facades\Artisan', + 'Auth' => 'Illuminate\Support\Facades\Auth', + 'Blade' => 'Illuminate\Support\Facades\Blade', + 'Bus' => 'Illuminate\Support\Facades\Bus', + 'Cache' => 'Illuminate\Support\Facades\Cache', + 'Config' => 'Illuminate\Support\Facades\Config', + 'Cookie' => 'Illuminate\Support\Facades\Cookie', + 'Crypt' => 'Illuminate\Support\Facades\Crypt', + 'DB' => 'Illuminate\Support\Facades\DB', + 'Eloquent' => 'Illuminate\Database\Eloquent\Model', + 'Event' => 'Illuminate\Support\Facades\Event', + 'File' => 'Illuminate\Support\Facades\File', + 'Hash' => 'Illuminate\Support\Facades\Hash', + 'Input' => 'Illuminate\Support\Facades\Input', + 'Inspiring' => 'Illuminate\Foundation\Inspiring', + 'Lang' => 'Illuminate\Support\Facades\Lang', + 'Log' => 'Illuminate\Support\Facades\Log', + 'Mail' => 'Illuminate\Support\Facades\Mail', + 'Password' => 'Illuminate\Support\Facades\Password', + 'Queue' => 'Illuminate\Support\Facades\Queue', + 'Redirect' => 'Illuminate\Support\Facades\Redirect', + 'Redis' => 'Illuminate\Support\Facades\Redis', + 'Request' => 'Illuminate\Support\Facades\Request', + 'Response' => 'Illuminate\Support\Facades\Response', + 'Route' => 'Illuminate\Support\Facades\Route', + 'Schema' => 'Illuminate\Support\Facades\Schema', + 'Session' => 'Illuminate\Support\Facades\Session', + 'Storage' => 'Illuminate\Support\Facades\Storage', + 'URL' => 'Illuminate\Support\Facades\URL', + 'Validator' => 'Illuminate\Support\Facades\Validator', + 'View' => 'Illuminate\Support\Facades\View', + + ], + +]; \ No newline at end of file diff --git a/config/auth.php b/config/auth.php new file mode 100644 index 00000000..ee6316fd --- /dev/null +++ b/config/auth.php @@ -0,0 +1,67 @@ + 'eloquent', + + /* + |-------------------------------------------------------------------------- + | Authentication Model + |-------------------------------------------------------------------------- + | + | When using the "Eloquent" authentication driver, we need to know which + | Eloquent model should be used to retrieve your users. Of course, it + | is often just the "User" model but you may use whatever you like. + | + */ + + 'model' => 'App\User', + + /* + |-------------------------------------------------------------------------- + | Authentication Table + |-------------------------------------------------------------------------- + | + | When using the "Database" authentication driver, we need to know which + | table should be used to retrieve your users. We have chosen a basic + | default value but you may easily change it to any table you like. + | + */ + + 'table' => 'users', + + /* + |-------------------------------------------------------------------------- + | Password Reset Settings + |-------------------------------------------------------------------------- + | + | Here you may set the options for resetting passwords including the view + | that is your password reset e-mail. You can also set the name of the + | table that maintains all of the reset tokens for your application. + | + | The expire time is the number of minutes that the reset token should be + | considered valid. This security feature keeps tokens short-lived so + | they have less time to be guessed. You may change this as needed. + | + */ + + 'password' => [ + 'email' => 'emails.password', + 'table' => 'password_resets', + 'expire' => 60, + ], + +]; \ No newline at end of file diff --git a/config/cache.php b/config/cache.php new file mode 100644 index 00000000..f9f5b576 --- /dev/null +++ b/config/cache.php @@ -0,0 +1,50 @@ + env('CACHE_DRIVER', 'redis'), + + /* + |-------------------------------------------------------------------------- + | Cache Stores + |-------------------------------------------------------------------------- + | + | Here you may define all of the cache "stores" for your application as + | well as their drivers. You may even define multiple stores for the + | same cache driver to group types of items stored in your caches. + | + */ + + 'stores' => [ + 'redis' => [ + 'driver' => 'redis', + 'connection' => 'default', + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Cache Key Prefix + |-------------------------------------------------------------------------- + | + | When utilizing a RAM based store such as APC or Memcached, there might + | be other applications utilizing the same cache. So, we'll specify a + | value to get prefixed to all our keys so we can avoid collisions. + | + */ + + 'prefix' => 'laravel', + +]; \ No newline at end of file diff --git a/config/compile.php b/config/compile.php new file mode 100644 index 00000000..fc20d82c --- /dev/null +++ b/config/compile.php @@ -0,0 +1,41 @@ + [ + + realpath(__DIR__.'/../app/Providers/AppServiceProvider.php'), + realpath(__DIR__.'/../app/Providers/BusServiceProvider.php'), + realpath(__DIR__.'/../app/Providers/ConfigServiceProvider.php'), + realpath(__DIR__.'/../app/Providers/EventServiceProvider.php'), + realpath(__DIR__.'/../app/Providers/RouteServiceProvider.php'), + + ], + + /* + |-------------------------------------------------------------------------- + | Compiled File Providers + |-------------------------------------------------------------------------- + | + | Here you may list service providers which define a "compiles" function + | that returns additional files that should be compiled, providing an + | easy way to get common files from any packages you are utilizing. + | + */ + + 'providers' => [ + // + ], + +]; \ No newline at end of file diff --git a/config/cors.php b/config/cors.php new file mode 100644 index 00000000..89f90ea8 --- /dev/null +++ b/config/cors.php @@ -0,0 +1,30 @@ + env('CORS_ALLOWED_HEADERS', 'origin, content-type, accept, authorization, x-requested-with'), + /** + * http://www.w3.org/TR/cors/#access-control-allow-methods-response-header + */ + 'allowed_methods' => env('CORS_ALLOWED_METHODS', 'GET, POST, OPTIONS, PUT, DELETE'), + 'use_pre_flight_caching' => env('CORS_USE_PRE_FLIGHT_CACHING', true), + /** + * http://www.w3.org/TR/cors/#access-control-max-age-response-header + */ + 'max_age' => env('CORS_MAX_AGE', 3200), + 'exposed_headers' => env('CORS_EXPOSED_HEADERS', ''), +); \ No newline at end of file diff --git a/config/curl.php b/config/curl.php new file mode 100644 index 00000000..3e1be332 --- /dev/null +++ b/config/curl.php @@ -0,0 +1,19 @@ + env('CURL_TIMEOUT', 60), + 'allow_redirects' => env('CURL_ALLOWS_REDIRECT', false), + 'verify_ssl_cert' => env('CURL_VERIFY_SSL_CERT', true), +); \ No newline at end of file diff --git a/config/database.php b/config/database.php new file mode 100644 index 00000000..299d15a5 --- /dev/null +++ b/config/database.php @@ -0,0 +1,109 @@ + PDO::FETCH_CLASS, + + /* + |-------------------------------------------------------------------------- + | Default Database Connection Name + |-------------------------------------------------------------------------- + | + | Here you may specify which of the database connections below you wish + | to use as your default connection for all database work. Of course + | you may use many connections at once using the Database library. + | + */ + + 'default' => 'openstackid_resources', + + /* + |-------------------------------------------------------------------------- + | Database Connections + |-------------------------------------------------------------------------- + | + | Here are each of the database connections setup for your application. + | Of course, examples of configuring each database platform that is + | supported by Laravel is shown below to make development simple. + | + | + | All database work in Laravel is done through the PHP PDO facilities + | so make sure you have the driver for your particular database of + | choice installed on your machine before you begin development. + | + */ + + 'connections' => [ + //primary DB + 'openstackid_resources' => array( + 'driver' => 'mysql', + 'host' => env('DB_HOST'), + 'database' => env('DB_DATABASE'), + 'username' => env('DB_USERNAME'), + 'password' => env('DB_PASSWORD'), + 'charset' => 'utf8', + 'collation' => 'utf8_unicode_ci', + 'prefix' => '', + ), + //secondary DB (SS OS) + 'ss' => array( + 'driver' => 'mysql', + 'host' => env('SS_DB_HOST'), + 'database' => env('SS_DATABASE'), + 'username' => env('SS_DB_USERNAME'), + 'password' => env('SS_DB_PASSWORD'), + 'charset' => 'utf8', + 'collation' => 'utf8_unicode_ci', + 'prefix' => '', + ), + ], + + /* + |-------------------------------------------------------------------------- + | Migration Repository Table + |-------------------------------------------------------------------------- + | + | This table keeps track of all the migrations that have already run for + | your application. Using this information, we can determine which of + | the migrations on disk haven't actually been run in the database. + | + */ + + 'migrations' => 'migrations', + + /* + |-------------------------------------------------------------------------- + | Redis Databases + |-------------------------------------------------------------------------- + | + | Redis is an open source, fast, and advanced key-value store that also + | provides a richer set of commands than a typical key-value systems + | such as APC or Memcached. Laravel makes it easy to dig right in. + | + */ + + 'redis' => [ + + 'cluster' => false, + + 'default' => [ + 'host' => env('REDIS_HOST'), + 'port' => env('REDIS_PORT'), + 'database' => env('REDIS_DB'), + 'password' => env('REDIS_PASSWORD'), + ], + + ], + +]; \ No newline at end of file diff --git a/config/filesystems.php b/config/filesystems.php new file mode 100644 index 00000000..300b790b --- /dev/null +++ b/config/filesystems.php @@ -0,0 +1,71 @@ + 'local', + + /* + |-------------------------------------------------------------------------- + | Default Cloud Filesystem Disk + |-------------------------------------------------------------------------- + | + | Many applications store files both locally and in the cloud. For this + | reason, you may specify a default "cloud" driver here. This driver + | will be bound as the Cloud disk implementation in the container. + | + */ + + 'cloud' => 's3', + + /* + |-------------------------------------------------------------------------- + | Filesystem Disks + |-------------------------------------------------------------------------- + | + | Here you may configure as many filesystem "disks" as you wish, and you + | may even configure multiple disks of the same driver. Defaults have + | been setup for each driver as an example of the required options. + | + */ + + 'disks' => [ + + 'local' => [ + 'driver' => 'local', + 'root' => storage_path().'/app', + ], + + 's3' => [ + 'driver' => 's3', + 'key' => 'your-key', + 'secret' => 'your-secret', + 'region' => 'your-region', + 'bucket' => 'your-bucket', + ], + + 'rackspace' => [ + 'driver' => 'rackspace', + 'username' => 'your-username', + 'key' => 'your-key', + 'container' => 'your-container', + 'endpoint' => 'https://identity.api.rackspacecloud.com/v2.0/', + 'region' => 'IAD', + 'url_type' => 'publicURL' + ], + + ], + +]; \ No newline at end of file diff --git a/config/log.php b/config/log.php new file mode 100644 index 00000000..130fafaf --- /dev/null +++ b/config/log.php @@ -0,0 +1,10 @@ + env('LOG_EMAIL_TO'), + //The sender of the mail + 'from_email' => env('LOG_EMAIL_FROM'), + ); \ No newline at end of file diff --git a/config/mail.php b/config/mail.php new file mode 100644 index 00000000..5905b1dd --- /dev/null +++ b/config/mail.php @@ -0,0 +1,124 @@ + env('MAIL_DRIVER', 'smtp'), + + /* + |-------------------------------------------------------------------------- + | SMTP Host Address + |-------------------------------------------------------------------------- + | + | Here you may provide the host address of the SMTP server used by your + | applications. A default option is provided that is compatible with + | the Mailgun mail service which will provide reliable deliveries. + | + */ + + 'host' => env('MAIL_HOST', 'smtp.mailgun.org'), + + /* + |-------------------------------------------------------------------------- + | SMTP Host Port + |-------------------------------------------------------------------------- + | + | This is the SMTP port used by your application to deliver e-mails to + | users of the application. Like the host we have set this value to + | stay compatible with the Mailgun e-mail application by default. + | + */ + + 'port' => env('MAIL_PORT', 587), + + /* + |-------------------------------------------------------------------------- + | Global "From" Address + |-------------------------------------------------------------------------- + | + | You may wish for all e-mails sent by your application to be sent from + | the same address. Here, you may specify a name and address that is + | used globally for all e-mails that are sent by your application. + | + */ + + 'from' => ['address' => null, 'name' => null], + + /* + |-------------------------------------------------------------------------- + | E-Mail Encryption Protocol + |-------------------------------------------------------------------------- + | + | Here you may specify the encryption protocol that should be used when + | the application send e-mail messages. A sensible default using the + | transport layer security protocol should provide great security. + | + */ + + 'encryption' => 'tls', + + /* + |-------------------------------------------------------------------------- + | SMTP Server Username + |-------------------------------------------------------------------------- + | + | If your SMTP server requires a username for authentication, you should + | set it here. This will get used to authenticate with your server on + | connection. You may also set the "password" value below this one. + | + */ + + 'username' => env('MAIL_USERNAME'), + + /* + |-------------------------------------------------------------------------- + | SMTP Server Password + |-------------------------------------------------------------------------- + | + | Here you may set the password required by your SMTP server to send out + | messages from your application. This will be given to the server on + | connection so that the application will be able to send messages. + | + */ + + 'password' => env('MAIL_PASSWORD'), + + /* + |-------------------------------------------------------------------------- + | Sendmail System Path + |-------------------------------------------------------------------------- + | + | When using the "sendmail" driver to send e-mails, we will need to know + | the path to where Sendmail lives on this server. A default path has + | been provided here, which will work well on most of your systems. + | + */ + + 'sendmail' => '/usr/sbin/sendmail -bs', + + /* + |-------------------------------------------------------------------------- + | Mail "Pretend" + |-------------------------------------------------------------------------- + | + | When this option is enabled, e-mail will not actually be sent over the + | web and will instead be written to your application's logs files so + | you may inspect the message. This is great for local development. + | + */ + + 'pretend' => false, + +]; \ No newline at end of file diff --git a/config/queue.php b/config/queue.php new file mode 100644 index 00000000..d5c7ea90 --- /dev/null +++ b/config/queue.php @@ -0,0 +1,92 @@ + env('QUEUE_DRIVER', 'sync'), + + /* + |-------------------------------------------------------------------------- + | Queue Connections + |-------------------------------------------------------------------------- + | + | Here you may configure the connection information for each server that + | is used by your application. A default configuration has been added + | for each back-end shipped with Laravel. You are free to add more. + | + */ + + 'connections' => [ + + 'sync' => [ + 'driver' => 'sync', + ], + + 'database' => [ + 'driver' => 'database', + 'table' => 'jobs', + 'queue' => 'default', + 'expire' => 60, + ], + + 'beanstalkd' => [ + 'driver' => 'beanstalkd', + 'host' => 'localhost', + 'queue' => 'default', + 'ttr' => 60, + ], + + 'sqs' => [ + 'driver' => 'sqs', + 'key' => 'your-public-key', + 'secret' => 'your-secret-key', + 'queue' => 'your-queue-url', + 'region' => 'us-east-1', + ], + + 'iron' => [ + 'driver' => 'iron', + 'host' => 'mq-aws-us-east-1.iron.io', + 'token' => 'your-token', + 'project' => 'your-project-id', + 'queue' => 'your-queue-name', + 'encrypt' => true, + ], + + 'redis' => [ + 'driver' => 'redis', + 'queue' => 'default', + 'expire' => 60, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Failed Queue Jobs + |-------------------------------------------------------------------------- + | + | These options configure the behavior of failed queue job logging so you + | can control which database and table are used to store the jobs that + | have failed. You may change them to any database / table you wish. + | + */ + + 'failed' => [ + 'database' => 'mysql', 'table' => 'failed_jobs', + ], + +]; \ No newline at end of file diff --git a/config/services.php b/config/services.php new file mode 100644 index 00000000..65abbac7 --- /dev/null +++ b/config/services.php @@ -0,0 +1,37 @@ + [ + 'domain' => '', + 'secret' => '', + ], + + 'mandrill' => [ + 'secret' => '', + ], + + 'ses' => [ + 'key' => '', + 'secret' => '', + 'region' => 'us-east-1', + ], + + 'stripe' => [ + 'model' => 'App\User', + 'secret' => '', + ], + +]; \ No newline at end of file diff --git a/config/session.php b/config/session.php new file mode 100644 index 00000000..901149db --- /dev/null +++ b/config/session.php @@ -0,0 +1,153 @@ + env('SESSION_DRIVER', 'redis'), + + /* + |-------------------------------------------------------------------------- + | Session Lifetime + |-------------------------------------------------------------------------- + | + | Here you may specify the number of minutes that you wish the session + | to be allowed to remain idle before it expires. If you want them + | to immediately expire on the browser closing, set that option. + | + */ + + 'lifetime' => 120, + + 'expire_on_close' => false, + + /* + |-------------------------------------------------------------------------- + | Session Encryption + |-------------------------------------------------------------------------- + | + | This option allows you to easily specify that all of your session data + | should be encrypted before it is stored. All encryption will be run + | automatically by Laravel and you can use the Session like normal. + | + */ + + 'encrypt' => false, + + /* + |-------------------------------------------------------------------------- + | Session File Location + |-------------------------------------------------------------------------- + | + | When using the native session driver, we need a location where session + | files may be stored. A default has been set for you but a different + | location may be specified. This is only needed for file sessions. + | + */ + + 'files' => storage_path().'/framework/sessions', + + /* + |-------------------------------------------------------------------------- + | Session Database Connection + |-------------------------------------------------------------------------- + | + | When using the "database" or "redis" session drivers, you may specify a + | connection that should be used to manage these sessions. This should + | correspond to a connection in your database configuration options. + | + */ + + 'connection' => null, + + /* + |-------------------------------------------------------------------------- + | Session Database Table + |-------------------------------------------------------------------------- + | + | When using the "database" session driver, you may specify the table we + | should use to manage the sessions. Of course, a sensible default is + | provided for you; however, you are free to change this as needed. + | + */ + + 'table' => 'sessions', + + /* + |-------------------------------------------------------------------------- + | Session Sweeping Lottery + |-------------------------------------------------------------------------- + | + | Some session drivers must manually sweep their storage location to get + | rid of old sessions from storage. Here are the chances that it will + | happen on a given request. By default, the odds are 2 out of 100. + | + */ + + 'lottery' => [2, 100], + + /* + |-------------------------------------------------------------------------- + | Session Cookie Name + |-------------------------------------------------------------------------- + | + | Here you may change the name of the cookie used to identify a session + | instance by ID. The name specified here will get used every time a + | new session cookie is created by the framework for every driver. + | + */ + + 'cookie' => 'openstackid_resources', + + /* + |-------------------------------------------------------------------------- + | Session Cookie Path + |-------------------------------------------------------------------------- + | + | The session cookie path determines the path for which the cookie will + | be regarded as available. Typically, this will be the root path of + | your application but you are free to change this when necessary. + | + */ + + 'path' => '/', + + /* + |-------------------------------------------------------------------------- + | Session Cookie Domain + |-------------------------------------------------------------------------- + | + | Here you may change the domain of the cookie used to identify a session + | in your application. This will determine which domains the cookie is + | available to in your application. A sensible default has been set. + | + */ + + 'domain' => env('SESSION_COOKIE_DOMAIN'), + + /* + |-------------------------------------------------------------------------- + | HTTPS Only Cookies + |-------------------------------------------------------------------------- + | + | By setting this option to true, session cookies will only be sent back + | to the server if the browser has a HTTPS connection. This will keep + | the cookie from being sent to you if it can not be done securely. + | + */ + + 'secure' => env('SESSION_COOKIE_SECURE', false), + +]; \ No newline at end of file diff --git a/config/view.php b/config/view.php new file mode 100644 index 00000000..48e9f929 --- /dev/null +++ b/config/view.php @@ -0,0 +1,33 @@ + [ + realpath(base_path('resources/views')) + ], + + /* + |-------------------------------------------------------------------------- + | Compiled View Path + |-------------------------------------------------------------------------- + | + | This option determines where all the compiled Blade templates will be + | stored for your application. Typically, this is within the storage + | directory. However, as usual, you are free to change this value. + | + */ + + 'compiled' => realpath(storage_path().'/framework/views'), + +]; \ No newline at end of file diff --git a/database/.gitignore b/database/.gitignore new file mode 100644 index 00000000..9b1dffd9 --- /dev/null +++ b/database/.gitignore @@ -0,0 +1 @@ +*.sqlite diff --git a/database/migrations/.gitkeep b/database/migrations/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/database/migrations/2015_04_27_141330_create_apis_table.php b/database/migrations/2015_04_27_141330_create_apis_table.php new file mode 100644 index 00000000..4617f615 --- /dev/null +++ b/database/migrations/2015_04_27_141330_create_apis_table.php @@ -0,0 +1,36 @@ +bigIncrements('id'); + $table->string('name',255)->unique(); + $table->text('description')->nullable(); + $table->boolean('active')->default(true); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('apis'); + } + +} diff --git a/database/migrations/2015_04_27_141832_create_api_scopes_table.php b/database/migrations/2015_04_27_141832_create_api_scopes_table.php new file mode 100644 index 00000000..ee5eeb55 --- /dev/null +++ b/database/migrations/2015_04_27_141832_create_api_scopes_table.php @@ -0,0 +1,50 @@ +bigIncrements('id'); + $table->string('name', 512); + $table->string('short_description', 512); + $table->text('description'); + $table->boolean('active')->default(true); + $table->boolean('default')->default(false); + $table->boolean('system')->default(false); + $table->timestamps(); + // FK + $table->bigInteger("api_id")->unsigned()->nullable(); + $table->index('api_id'); + $table->foreign('api_id') + ->references('id') + ->on('apis') + ->onDelete('cascade') + ->onUpdate('no action'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('api_scopes', function ($table) { + $table->dropForeign('api_id'); + }); + + Schema::drop('api_scopes'); + } + +} diff --git a/database/migrations/2015_04_27_141848_create_api_endpoints_table.php b/database/migrations/2015_04_27_141848_create_api_endpoints_table.php new file mode 100644 index 00000000..878362d7 --- /dev/null +++ b/database/migrations/2015_04_27_141848_create_api_endpoints_table.php @@ -0,0 +1,81 @@ +bigIncrements('id'); + $table->boolean('active')->default(true); + $table->boolean('allow_cors')->default(true); + $table->boolean('allow_credentials')->default(true); + $table->text('description')->nullable(); + $table->string('name', 255)->unique(); + $table->timestamps(); + $table->text("route"); + $table->enum('http_method', array('GET', 'HEAD','POST', 'PUT', 'DELETE', 'TRACE', 'CONNECT', 'OPTIONS', 'PATCH')); + $table->bigInteger("rate_limit")->unsigned()->nullable(); + //FK + $table->bigInteger("api_id")->unsigned(); + $table->index('api_id'); + $table->foreign('api_id') + ->references('id') + ->on('apis') + ->onDelete('cascade') + ->onUpdate('no action'); + }); + + Schema::create('endpoint_api_scopes', function ($table) { + $table->timestamps(); + $table->bigInteger("api_endpoint_id")->unsigned(); + $table->index('api_endpoint_id'); + $table->foreign('api_endpoint_id') + ->references('id') + ->on('api_endpoints') + ->onDelete('cascade') + ->onUpdate('no action');; + // FK 2 + $table->bigInteger("scope_id")->unsigned(); + $table->index('scope_id'); + $table->foreign('scope_id') + ->references('id') + ->on('api_scopes') + ->onDelete('cascade') + ->onUpdate('no action'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('endpoint_api_scopes', function ($table) { + $table->dropForeign('api_endpoint_id'); + }); + + Schema::table('endpoint_api_scopes', function ($table) { + $table->dropForeign('scope_id'); + }); + + Schema::dropIfExists('endpoint_api_scopes'); + + Schema::table('api_endpoints', function ($table) { + $table->dropForeign('api_id'); + }); + + Schema::drop('api_endpoints'); + } + +} \ No newline at end of file diff --git a/database/seeds/.gitkeep b/database/seeds/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/database/seeds/ApiEndpointsSeeder.php b/database/seeds/ApiEndpointsSeeder.php new file mode 100644 index 00000000..aef51ed4 --- /dev/null +++ b/database/seeds/ApiEndpointsSeeder.php @@ -0,0 +1,185 @@ +delete(); + DB::table('api_endpoints')->delete(); + + $this->seedPublicCloudsEndpoints(); + $this->seedPrivateCloudsEndpoints(); + $this->seedConsultantsEndpoints(); + } + + private function seedPublicCloudsEndpoints() + { + + $public_clouds = Api::where('name', '=', 'public-clouds')->first(); + $current_realm = Config::get('app.url'); + // endpoints scopes + + ApiEndpoint::create( + array( + 'name' => 'get-public-clouds', + 'active' => true, + 'api_id' => $public_clouds->id, + 'route' => '/api/v1/marketplace/public-clouds', + 'http_method' => 'GET' + ) + ); + + ApiEndpoint::create( + array( + 'name' => 'get-public-cloud', + 'active' => true, + 'api_id' => $public_clouds->id, + 'route' => '/api/v1/marketplace/public-clouds/{id}', + 'http_method' => 'GET' + ) + ); + + ApiEndpoint::create( + array( + 'name' => 'get-public-cloud-datacenters', + 'active' => true, + 'api_id' => $public_clouds->id, + 'route' => '/api/v1/marketplace/public-clouds/{id}/data-centers', + 'http_method' => 'GET' + ) + ); + + $public_cloud_read_scope = ApiScope::where('name', '=', sprintf('%s/public-clouds/read', $current_realm))->first(); + + $endpoint_get_public_clouds = ApiEndpoint::where('name', '=', 'get-public-clouds')->first(); + $endpoint_get_public_clouds->scopes()->attach($public_cloud_read_scope->id); + + $endpoint_get_public_cloud = ApiEndpoint::where('name', '=', 'get-public-cloud')->first(); + $endpoint_get_public_cloud->scopes()->attach($public_cloud_read_scope->id); + + $endpoint_get_public_cloud_datacenters = ApiEndpoint::where('name', '=', 'get-public-cloud-datacenters')->first(); + $endpoint_get_public_cloud_datacenters->scopes()->attach($public_cloud_read_scope->id); + } + + private function seedPrivateCloudsEndpoints() + { + $private_clouds = Api::where('name', '=', 'private-clouds')->first(); + $current_realm = Config::get('app.url'); + // endpoints scopes + + ApiEndpoint::create( + array( + 'name' => 'get-private-clouds', + 'active' => true, + 'api_id' => $private_clouds->id, + 'route' => '/api/v1/marketplace/private-clouds', + 'http_method' => 'GET' + ) + ); + + ApiEndpoint::create( + array( + 'name' => 'get-private-cloud', + 'active' => true, + 'api_id' => $private_clouds->id, + 'route' => '/api/v1/marketplace/private-clouds/{id}', + 'http_method' => 'GET' + ) + ); + + ApiEndpoint::create( + array( + 'name' => 'get-private-cloud-datacenters', + 'active' => true, + 'api_id' => $private_clouds->id, + 'route' => '/api/v1/marketplace/private-clouds/{id}/data-centers', + 'http_method' => 'GET' + ) + ); + + $private_cloud_read_scope = ApiScope::where('name', '=', sprintf('%s/private-clouds/read', $current_realm))->first(); + + $endpoint_get_private_clouds = ApiEndpoint::where('name', '=', 'get-private-clouds')->first(); + $endpoint_get_private_clouds->scopes()->attach($private_cloud_read_scope->id); + + $endpoint_get_private_cloud = ApiEndpoint::where('name', '=', 'get-private-cloud')->first(); + $endpoint_get_private_cloud->scopes()->attach($private_cloud_read_scope->id); + + $endpoint_get_private_cloud_datacenters = ApiEndpoint::where('name', '=', 'get-private-cloud-datacenters')->first(); + $endpoint_get_private_cloud_datacenters->scopes()->attach($private_cloud_read_scope->id); + + } + + private function seedConsultantsEndpoints() + { + + $consultants = Api::where('name', '=', 'consultants')->first(); + $current_realm = Config::get('app.url'); + // endpoints scopes + + ApiEndpoint::create( + array( + 'name' => 'get-consultants', + 'active' => true, + 'api_id' => $consultants->id, + 'route' => '/api/v1/marketplace/consultants', + 'http_method' => 'GET' + ) + ); + + ApiEndpoint::create( + array( + 'name' => 'get-consultant', + 'active' => true, + 'api_id' => $consultants->id, + 'route' => '/api/v1/marketplace/consultants/{id}', + 'http_method' => 'GET' + ) + ); + + ApiEndpoint::create( + array( + 'name' => 'get-consultant-offices', + 'active' => true, + 'api_id' => $consultants->id, + 'route' => '/api/v1/marketplace/consultants/{id}/offices', + 'http_method' => 'GET' + ) + ); + + $consultant_read_scope = ApiScope::where('name', '=', sprintf('%s/consultants/read', $current_realm))->first(); + + $endpoint = ApiEndpoint::where('name', '=', 'get-consultants')->first(); + $endpoint->scopes()->attach($consultant_read_scope->id); + + $endpoint = ApiEndpoint::where('name', '=', 'get-consultant')->first(); + $endpoint->scopes()->attach($consultant_read_scope->id); + + $endpoint = ApiEndpoint::where('name', '=', 'get-consultant-offices')->first(); + $endpoint->scopes()->attach($consultant_read_scope->id); + } + +} \ No newline at end of file diff --git a/database/seeds/ApiScopesSeeder.php b/database/seeds/ApiScopesSeeder.php new file mode 100644 index 00000000..652fc5ef --- /dev/null +++ b/database/seeds/ApiScopesSeeder.php @@ -0,0 +1,88 @@ +delete(); + DB::table('api_scopes')->delete(); + + $this->seedPublicCloudScopes(); + $this->seedPrivateCloudScopes(); + $this->seedConsultantScopes(); + } + + private function seedPublicCloudScopes() + { + + $current_realm = Config::get('app.url'); + $public_clouds = Api::where('name', '=', 'public-clouds')->first(); + + ApiScope::create( + array( + 'name' => sprintf('%s/public-clouds/read', $current_realm), + 'short_description' => 'Get Public Clouds', + 'description' => 'Grants read only access for Public Clouds', + 'api_id' => $public_clouds->id, + 'system' => false + ) + ); + } + + private function seedPrivateCloudScopes() + { + + $current_realm = Config::get('app.url'); + $private_clouds = Api::where('name', '=', 'private-clouds')->first(); + + ApiScope::create( + array( + 'name' => sprintf('%s/private-clouds/read', $current_realm), + 'short_description' => 'Get Private Clouds', + 'description' => 'Grants read only access for Private Clouds', + 'api_id' => $private_clouds->id, + 'system' => false + ) + ); + } + + private function seedConsultantScopes() + { + + $current_realm = Config::get('app.url'); + $consultants = Api::where('name', '=', 'consultants')->first(); + + ApiScope::create( + array( + 'name' => sprintf('%s/consultants/read', $current_realm), + 'short_description' => 'Get Consultants', + 'description' => 'Grants read only access for Consultants', + 'api_id' => $consultants->id, + 'system' => false + ) + ); + } + +} \ No newline at end of file diff --git a/database/seeds/ApiSeeder.php b/database/seeds/ApiSeeder.php new file mode 100644 index 00000000..f6b6d7e5 --- /dev/null +++ b/database/seeds/ApiSeeder.php @@ -0,0 +1,54 @@ +delete(); + DB::table('api_scopes')->delete(); + DB::table('api_endpoints')->delete(); + DB::table('apis')->delete(); + // public clouds + Api::create( + array( + 'name' => 'public-clouds', + 'active' => true, + 'Description' => 'Marketplace Public Clouds' + ) + ); + // private clouds + Api::create( + array( + 'name' => 'private-clouds', + 'active' => true, + 'Description' => 'Marketplace Private Clouds' + ) + ); + // consultants + Api::create( + array( + 'name' => 'consultants', + 'active' => true, + 'Description' => 'Marketplace Consultants' + ) + ); + } +} \ No newline at end of file diff --git a/database/seeds/DatabaseSeeder.php b/database/seeds/DatabaseSeeder.php new file mode 100644 index 00000000..a4a72eda --- /dev/null +++ b/database/seeds/DatabaseSeeder.php @@ -0,0 +1,22 @@ +call('ApiSeeder'); + $this->call('ApiScopesSeeder'); + $this->call('ApiEndpointsSeeder'); + } + +} diff --git a/database/seeds/TestSeeder.php b/database/seeds/TestSeeder.php new file mode 100644 index 00000000..60596f82 --- /dev/null +++ b/database/seeds/TestSeeder.php @@ -0,0 +1,32 @@ +call('ApiSeeder'); + $this->call('ApiScopesSeeder'); + $this->call('ApiEndpointsSeeder'); + } + +} \ No newline at end of file diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100644 index 00000000..b369b7cf --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import datetime +import os +import sys + +sys.path.insert(0, os.path.abspath('../..')) +# -- General configuration ---------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinxcontrib.httpdomain', + #'sphinx.ext.intersphinx', + 'oslosphinx', + 'yasfb', +] + +# Feed configuration for yasfb +feed_base_url = 'http://ci.openstack.org/openstackid-resources' +feed_author = 'OpenStack Infrastructure Team' + +exclude_patterns = [ + 'template.rst', +] + +# Optionally allow the use of sphinxcontrib.spelling to verify the +# spelling of the documents. +try: + import sphinxcontrib.spelling + extensions.append('sphinxcontrib.spelling') +except ImportError: + pass + +# autodoc generation is a bit aggressive and a nuisance when doing heavy +# text edit cycles. +# execute "export SPHINX_DEBUG=1" in your terminal to disable + +# The suffix of source filenames. +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'openstackid-resources' +copyright = u'%s, OpenStack Foundation' % datetime.date.today().year + +# If true, '()' will be appended to :func: etc. cross-reference text. +add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +add_module_names = True + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# -- Options for HTML output -------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. Major themes that come with +# Sphinx are currently 'default' and 'sphinxdoc'. +# html_theme_path = ["."] +# html_theme = '_theme' +# html_static_path = ['static'] + +# Output file base name for HTML help builder. +htmlhelp_basename = '%sdoc' % project + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass +# [howto/manual]). +latex_documents = [ + ('index', + '%s.tex' % project, + u'%s Documentation' % project, + u'OpenStack Foundation', 'manual'), +] + +# Example configuration for intersphinx: refer to the Python standard library. +#intersphinx_mapping = {'http://docs.python.org/': None} \ No newline at end of file diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 00000000..0253d7c7 --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,17 @@ +======================================================= +Welcome to OpenStackId's Resource Server documentation! +======================================================= + +Introduction +============ + +Table of contents +================= + +Client API Reference +-------------------- + +.. toctree:: + :maxdepth: 1 + + restapi/v1 \ No newline at end of file diff --git a/doc/source/restapi/v1.rst b/doc/source/restapi/v1.rst new file mode 100644 index 00000000..0ca4ed0c --- /dev/null +++ b/doc/source/restapi/v1.rst @@ -0,0 +1,560 @@ +================== +OAuth 2.0 Rest API +================== + +Schema +^^^^^^ + +All API access is over HTTPS, and accessed from the **https://openstackid-resources.org/** +domain. All data is sent and received as JSON. + +Parameters +^^^^^^^^^^ + +Many API methods take optional parameters. For GET requests, any parameters not +specified as a segment in the path can be passed as an HTTP query string + +Pagination +^^^^^^^^^^ + +Requests that return multiple items will be paginated to 10 items by default. +You can specify further pages with the **?page** parameter. For some +resources, you can also set a custom page size up to 100 with the **?per_page** +parameter. + +Rate Limiting +^^^^^^^^^^^^^ + +This is configured per API endpoint. +You can check the returned HTTP headers of any API request to see your current +rate limit status:: + + X-RateLimit-Limit: 60 + X-RateLimit-Remaining: 56 + X-RateLimit-Reset: 1372700873 + + +The headers tell you everything you need to know about your current rate limit +status : + +======================= ============================================================================== +Header Name Description +======================= ============================================================================== +X-RateLimit-Limit The maximum number of requests that the consumer is permitted to make per hour. +X-RateLimit-Remaining The number of requests remaining in the current rate limit window. +X-RateLimit-Reset The number of seconds remaining until the current rate limit window resets. +======================= ============================================================================== + +If your application triggers this rate limit, you'll receive an informative +response: + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 403 Forbidden + Content-Type: application/json; charset=utf-8 + Connection: close + + { + + "message": "You have triggered an abuse detection mechanism and have been + temporarily blocked. Please retry your request again later." + + } + +Conditional requests +^^^^^^^^^^^^^^^^^^^^ + +Most responses return an **ETag** header. You can use the values +of this headers to make subsequent requests to those resources using the +**If-None-Match** header, respectively. If the resource +has not changed, the server will return a **304 Not Modified**. + + +Cross Origin Resource Sharing +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The API supports Cross Origin Resource Sharing (CORS) for AJAX requests from +any origin. +You can read the [CORS W3C Recommendation](http://www.w3.org/TR/cors), or +[this intro] +(http://code.google.com/p/html5security/wiki/CrossOriginRequestSecurity) from +the HTML 5 Security Guide. + +JSON-P Callbacks +^^^^^^^^^^^^^^^^ + +You can send a **?callback** parameter to any GET call to have the results +wrapped in a JSON function. This is typically used when browsers want to +embed OpenStack content in web pages by getting around cross domain issues. +The response includes the same data output as the regular API, plus the +relevant HTTP Header information. + + +MarketPlace API +^^^^^^^^^^^^^^^ + +Public Clouds Endpoints +----------------------- + +Allows to get read only access to public clouds related data ( clouds and data +centers locations) + +.. http:get:: /api/v1/marketplace/public-clouds + + Get a list of public clouds + + **Example request**: + + .. sourcecode:: http + + GET /api/v1/marketplace/public-clouds HTTP/1.1 + Host: openstackid.org + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: text/javascript + + { + + "total":20, + "per_page":10, + "current_page":1, + "last_page":2, + "from":1, + "to":10, + "data":[ + { + "ID":"YYYY", + "Created":"2014-04-23 05:36:10", + "LastEdited":"2015-02-04 11:13:58", + "Name":"Next-Generation AgileCLOUD", + "Slug":"next-generation-agilecloud", + "Overview":"....", + "Call2ActionUri":"http:\/\/....", + "Active":"1", + "CompanyID":"XXX" + } + ,{...} + ] + + } + + :query page: used in conjunction with "per_page" query string parameter. + indicates the desired page number, when we want paginate + over results + :query per_page: used in conjunction with "page" query string parameter. + indicates the desired page size + :query status: (optional filter) allow us to get active, non active or all + public clouds + :query order_by: (optional) used in conjunction with query string parameter + "order_dir", point out the desired order of the result (date or name) + :query order_dir: (optional) used in conjunction with query string parameter + "order", point out the desired order direction of the result (asc or desc) + :reqheader Authorization: OAuth 2.0 Bearer Access Token + + :statuscode 200: no error + :statuscode 412: invalid parameters + :statuscode 500: server error + +.. http:get:: api/v1/marketplace/public-clouds/(int:id) + + Get desired public cloud point out by `id` param + + **Example request**: + + .. sourcecode:: http + + GET /api/v1/marketplace/public-clouds/123456 HTTP/1.1 + Host: openstackid.org + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "ID":"123456", + "Created":"2014-04-23 05:36:10", + "LastEdited":"2015-02-04 11:13:58", + "Name":"test public cloud", + "Slug":"test-public-cloud", + "Overview":"lorep ip sum", + "Call2ActionUri":"http:\/\/.../", + "Active":"1", + "CompanyID":"123456" + } + + :reqheader Authorization: OAuth 2.0 Bearer Access Token + + :statuscode 200: no error + :statuscode 404: entity not found + :statuscode 500: server error + + +.. http:get:: /api/v1/marketplace/public-clouds/(int:id)/data-centers + + Get data center locations for public cloud pointed out by `id` param + + **Example request**: + + .. sourcecode:: http + + GET /api/v1/marketplace/public-clouds/123456/data-centers HTTP/1.1 + Host: openstackid.org + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + {"datacenters":[ + { + "ID":"72", + "Created":"2014-05-07 15:19:39", + "LastEdited":"2014-05-07 15:19:39", + "Name":"West", + "Endpoint":"https:\/\/identity.uswest1.cloud.io.com\/v2.0", + "Color":"000000", + "locations":[ + { + "ID":"109", + "Created":"2014-05-07 15:19:39", + "LastEdited":"2014-05-07 15:19:39", + "City":"Phoenix", + "State":"AZ", + "Country":"US", + "Lat":"33.45", + "Lng":"-112.07" + } + ] + },... + ] + } + + :reqheader Authorization: OAuth 2.0 Bearer Access Token + + :statuscode 200: no error + :statuscode 404: entity not found (cloud) + :statuscode 500: server error + +Private Clouds Endpoints +------------------------ + +Allows to get read only access to private clouds related data ( clouds and data +centers locations) + +.. http:get:: /api/v1/marketplace/private-clouds + + Get a list of private clouds + + **Example request**: + + .. sourcecode:: http + + GET /api/v1/marketplace/private-clouds HTTP/1.1 + Host: openstackid.org + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: text/javascript + + { + + "total":20, + "per_page":10, + "current_page":1, + "last_page":2, + "from":1, + "to":10, + "data":[ + { + "ID":"YYYY", + "Created":"2014-04-23 05:36:10", + "LastEdited":"2015-02-04 11:13:58", + "Name":"test private cloud", + "Slug":"test-private-cloud", + "Overview":"....", + "Call2ActionUri":"http:\/\/....", + "Active":"1", + "CompanyID":"XXX" + } + ,{...} + ] + + } + + :query page: used in conjunction with "per_page" query string parameter. + indicates the desired page number, when we want paginate + over results + :query per_page: used in conjunction with "page" query string parameter. + indicates the desired page size + :query status: (optional filter) allow us to get active, non active or all + public clouds + :query order_by: (optional) used in conjunction with query string parameter + "order_dir", point out the desired order of the result (date or name) + :query order_dir: (optional) used in conjunction with query string parameter + "order", point out the desired order direction of the result (asc or desc) + + :reqheader Authorization: OAuth 2.0 Bearer Access Token + + :statuscode 200: no error + :statuscode 412: invalid parameters + :statuscode 500: server error + +.. http:get:: /api/v1/marketplace/private-clouds/(int:id) + + Get desired private cloud point out by `id` param + + **Example request**: + + .. sourcecode:: http + + GET /api/v1/marketplace/private-clouds/123456 HTTP/1.1 + Host: openstackid.org + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "ID":"123456", + "Created":"2014-04-23 05:36:10", + "LastEdited":"2015-02-04 11:13:58", + "Name":"test private cloud", + "Slug":"test-private-cloud", + "Overview":"lorep ip sum", + "Call2ActionUri":"http:\/\/..", + "Active":"1", + "CompanyID":"123456" + } + + :reqheader Authorization: OAuth 2.0 Bearer Access Token + + :statuscode 200: no error + :statuscode 404: entity not found + :statuscode 500: server error + + +.. http:get:: /api/v1/marketplace/private-clouds/(int:id)/data-centers + + Get data center locations for private cloud pointed out by `id` param + + **Example request**: + + .. sourcecode:: http + + GET /api/v1/marketplace/private-clouds/123456/data-centers HTTP/1.1 + Host: openstackid.org + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + {"datacenters":[ + { + "ID":"72", + "Created":"2014-05-07 15:19:39", + "LastEdited":"2014-05-07 15:19:39", + "Name":"West", + "Endpoint":"https:\/\/identity.uswest1.cloud.io.com\/v2.0", + "Color":"000000", + "locations":[ + { + "ID":"109", + "Created":"2014-05-07 15:19:39", + "LastEdited":"2014-05-07 15:19:39", + "City":"Phoenix", + "State":"AZ", + "Country":"US", + "Lat":"33.45", + "Lng":"-112.07" + } + ] + },... + ] + } + + :reqheader Authorization: OAuth 2.0 Bearer Access Token + + :statuscode 200: no error + :statuscode 404: entity not found (cloud) + :statuscode 500: server error + + +Consultants Endpoints +--------------------- + +Allows to get read only access to consultants related data ( consultants and +offices locations) + +.. http:get:: /api/v1/marketplace/consultants + + Get a list of consultants + + **Example request**: + + .. sourcecode:: http + + GET /api/v1/marketplace/consultants HTTP/1.1 + Host: openstackid.org + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: text/javascript + + { + + "total":20, + "per_page":10, + "current_page":1, + "last_page":2, + "from":1, + "to":10, + "data":[ + { + "ID":"YYYY", + "Created":"2014-04-23 05:36:10", + "LastEdited":"2015-02-04 11:13:58", + "Name":"Consultant Name", + "Slug":"consultant-name", + "Overview":"....", + "Call2ActionUri":"http:\/\/....", + "Active":"1", + "CompanyID":"XXX" + } + ,{...} + ] + + } + + :query page: used in conjunction with "per_page" query string parameter. + indicates the desired page number, when we want paginate + over results + :query per_page: used in conjunction with "page" query string parameter. + indicates the desired page size + :query status: (optional filter) allow us to get active, non active or all + public clouds + :query order_by: (optional) used in conjunction with query string parameter + "order_dir", point out the desired order of the result (date or name) + :query order_dir: (optional) used in conjunction with query string parameter + "order", point out the desired order direction of the result (asc or desc) + + :reqheader Authorization: OAuth 2.0 Bearer Access Token + + :statuscode 200: no error + :statuscode 412: invalid parameters + :statuscode 500: server error + +.. http:get:: /api/v1/marketplace/consultants/(int:id) + + Get desired consultant point out by `id` param + + **Example request**: + + .. sourcecode:: http + + GET /api/v1/marketplace/consultants/123456 HTTP/1.1 + Host: openstackid.org + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "ID":"123456", + "Created":"2014-04-23 05:36:10", + "LastEdited":"2015-02-04 11:13:58", + "Name":"Consultant Name", + "Slug":"consultant_name", + "Overview":"lorep ip sum", + "Call2ActionUri":"http:\/\/...", + "Active":"1", + "CompanyID":"123456" + } + + :reqheader Authorization: OAuth 2.0 Bearer Access Token + + :statuscode 200: no error + :statuscode 404: entity not found + :statuscode 500: server error + +.. http:get:: /api/v1/marketplace/consultants/(int:id)/offices + + Get offices locations for consultant pointed out by `id` param + + **Example request**: + + .. sourcecode:: http + + GET /api/v1/marketplace/consultants/123456/offices HTTP/1.1 + Host: openstackid.org + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "offices":[ + { + "ID":"45", + "Created":"2014-04-29 16:02:50", + "LastEdited":"2014-04-29 16:02:50", + "Address":null, + "Address2":null, + "State":"CA", + "ZipCode":null, + "City":"Mountain View", + "Country":"US", + "Lat":"37.39", + "Lng":"-122.08" + },... + ] + } + + :reqheader Authorization: OAuth 2.0 Bearer Access Token + + :statuscode 200: no error + :statuscode 404: entity not found (consultant) + :statuscode 500: server error \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 00000000..7cf62673 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,16 @@ +var elixir = require('laravel-elixir'); + +/* + |-------------------------------------------------------------------------- + | Elixir Asset Management + |-------------------------------------------------------------------------- + | + | Elixir provides a clean, fluent API for defining some basic Gulp tasks + | for your Laravel application. By default, we are compiling the Less + | file for our application, as well as publishing vendor resources. + | + */ + +elixir(function(mix) { + mix.less('app.less'); +}); diff --git a/package.json b/package.json new file mode 100644 index 00000000..5595f071 --- /dev/null +++ b/package.json @@ -0,0 +1,7 @@ +{ + "private": true, + "devDependencies": { + "gulp": "^3.8.8", + "laravel-elixir": "*" + } +} diff --git a/phpspec.yml b/phpspec.yml new file mode 100644 index 00000000..eb57939e --- /dev/null +++ b/phpspec.yml @@ -0,0 +1,5 @@ +suites: + main: + namespace: App + psr4_prefix: App + src_path: app \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 00000000..b22af540 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,20 @@ + + + + + ./tests/ + + + + + + diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 00000000..77827ae7 --- /dev/null +++ b/public/.htaccess @@ -0,0 +1,15 @@ + + + Options -MultiViews + + + RewriteEngine On + + # Redirect Trailing Slashes... + RewriteRule ^(.*)/$ /$1 [L,R=301] + + # Handle Front Controller... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [L] + diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 00000000..e69de29b diff --git a/public/index.php b/public/index.php new file mode 100644 index 00000000..37f19c23 --- /dev/null +++ b/public/index.php @@ -0,0 +1,57 @@ + + */ + +/* +|-------------------------------------------------------------------------- +| Register The Auto Loader +|-------------------------------------------------------------------------- +| +| Composer provides a convenient, automatically generated class loader for +| our application. We just need to utilize it! We'll simply require it +| into the script here so that we don't have to worry about manual +| loading any of our classes later on. It feels nice to relax. +| +*/ + +require __DIR__.'/../bootstrap/autoload.php'; + +/* +|-------------------------------------------------------------------------- +| Turn On The Lights +|-------------------------------------------------------------------------- +| +| We need to illuminate PHP development, so let us turn on the lights. +| This bootstraps the framework and gets it ready for use, then it +| will load up this application so that we can run it and send +| the responses back to the browser and delight our users. +| +*/ + +$app = require_once __DIR__.'/../bootstrap/app.php'; + +/* +|-------------------------------------------------------------------------- +| Run The Application +|-------------------------------------------------------------------------- +| +| Once we have the application, we can handle the incoming request +| through the kernel, and send the associated response back to +| the client's browser allowing them to enjoy the creative +| and wonderful application we have prepared for them. +| +*/ + +$kernel = $app->make('Illuminate\Contracts\Http\Kernel'); + +$response = $kernel->handle( + $request = Illuminate\Http\Request::capture() +); + +$response->send(); + +$kernel->terminate($request, $response); diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 00000000..eb053628 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/readme.md b/readme.md new file mode 100644 index 00000000..a322e088 --- /dev/null +++ b/readme.md @@ -0,0 +1,23 @@ +# OpenStackId Resource Server + +## Prerequisites + + * LAMP/LEMP environment + * PHP >= 5.4.0 + * Redis + * composer (https://getcomposer.org/) + +## Install + +run following commands on root folder + * curl -s https://getcomposer.org/installer | php + * php composer.phar install --prefer-dist + * php composer.phar dump-autoload --optimize + * php artisan migrate --env=YOUR_ENVIRONMENT + * php artisan db:seed --env=YOUR_ENVIRONMENT + * phpunit --bootstrap vendor/autoload.php + * give proper rights to storage folder (775 and proper users) + +## Permissions + +Laravel may require some permissions to be configured: folders within storage and vendor require write access by the web server. diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..585de733 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +oslosphinx +sphinx>=1.1.2,<1.2 +sphinxcontrib-httpdomain +yasfb>=0.5.1 \ No newline at end of file diff --git a/resources/assets/less/app.less b/resources/assets/less/app.less new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/resources/assets/less/app.less @@ -0,0 +1 @@ + diff --git a/resources/lang/en/pagination.php b/resources/lang/en/pagination.php new file mode 100644 index 00000000..13b4dcb3 --- /dev/null +++ b/resources/lang/en/pagination.php @@ -0,0 +1,19 @@ + '« Previous', + 'next' => 'Next »', + +]; diff --git a/resources/lang/en/passwords.php b/resources/lang/en/passwords.php new file mode 100644 index 00000000..1fc0e1ef --- /dev/null +++ b/resources/lang/en/passwords.php @@ -0,0 +1,22 @@ + "Passwords must be at least six characters and match the confirmation.", + "user" => "We can't find a user with that e-mail address.", + "token" => "This password reset token is invalid.", + "sent" => "We have e-mailed your password reset link!", + "reset" => "Your password has been reset!", + +]; diff --git a/resources/lang/en/validation.php b/resources/lang/en/validation.php new file mode 100644 index 00000000..764f0563 --- /dev/null +++ b/resources/lang/en/validation.php @@ -0,0 +1,107 @@ + "The :attribute must be accepted.", + "active_url" => "The :attribute is not a valid URL.", + "after" => "The :attribute must be a date after :date.", + "alpha" => "The :attribute may only contain letters.", + "alpha_dash" => "The :attribute may only contain letters, numbers, and dashes.", + "alpha_num" => "The :attribute may only contain letters and numbers.", + "array" => "The :attribute must be an array.", + "before" => "The :attribute must be a date before :date.", + "between" => [ + "numeric" => "The :attribute must be between :min and :max.", + "file" => "The :attribute must be between :min and :max kilobytes.", + "string" => "The :attribute must be between :min and :max characters.", + "array" => "The :attribute must have between :min and :max items.", + ], + "boolean" => "The :attribute field must be true or false.", + "confirmed" => "The :attribute confirmation does not match.", + "date" => "The :attribute is not a valid date.", + "date_format" => "The :attribute does not match the format :format.", + "different" => "The :attribute and :other must be different.", + "digits" => "The :attribute must be :digits digits.", + "digits_between" => "The :attribute must be between :min and :max digits.", + "email" => "The :attribute must be a valid email address.", + "filled" => "The :attribute field is required.", + "exists" => "The selected :attribute is invalid.", + "image" => "The :attribute must be an image.", + "in" => "The selected :attribute is invalid.", + "integer" => "The :attribute must be an integer.", + "ip" => "The :attribute must be a valid IP address.", + "max" => [ + "numeric" => "The :attribute may not be greater than :max.", + "file" => "The :attribute may not be greater than :max kilobytes.", + "string" => "The :attribute may not be greater than :max characters.", + "array" => "The :attribute may not have more than :max items.", + ], + "mimes" => "The :attribute must be a file of type: :values.", + "min" => [ + "numeric" => "The :attribute must be at least :min.", + "file" => "The :attribute must be at least :min kilobytes.", + "string" => "The :attribute must be at least :min characters.", + "array" => "The :attribute must have at least :min items.", + ], + "not_in" => "The selected :attribute is invalid.", + "numeric" => "The :attribute must be a number.", + "regex" => "The :attribute format is invalid.", + "required" => "The :attribute field is required.", + "required_if" => "The :attribute field is required when :other is :value.", + "required_with" => "The :attribute field is required when :values is present.", + "required_with_all" => "The :attribute field is required when :values is present.", + "required_without" => "The :attribute field is required when :values is not present.", + "required_without_all" => "The :attribute field is required when none of :values are present.", + "same" => "The :attribute and :other must match.", + "size" => [ + "numeric" => "The :attribute must be :size.", + "file" => "The :attribute must be :size kilobytes.", + "string" => "The :attribute must be :size characters.", + "array" => "The :attribute must contain :size items.", + ], + "unique" => "The :attribute has already been taken.", + "url" => "The :attribute format is invalid.", + "timezone" => "The :attribute must be a valid zone.", + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap attribute place-holders + | with something more reader friendly such as E-Mail Address instead + | of "email". This simply helps us make messages a little cleaner. + | + */ + + 'attributes' => [], + +]; diff --git a/resources/views/errors/404.blade.php b/resources/views/errors/404.blade.php new file mode 100644 index 00000000..f1bd62f0 --- /dev/null +++ b/resources/views/errors/404.blade.php @@ -0,0 +1,12 @@ +@extends('layouts.master') +@section('title', '404') +@section('content') +
+

+ 404. That's an error. +

+

+ The page you requested is invalid. That's all we know. +

+
+@stop \ No newline at end of file diff --git a/resources/views/errors/503.blade.php b/resources/views/errors/503.blade.php new file mode 100644 index 00000000..669dcb80 --- /dev/null +++ b/resources/views/errors/503.blade.php @@ -0,0 +1,41 @@ + + + + + + + +
+
+
Be right back.
+
+
+ + diff --git a/resources/views/layouts/master.blade.php b/resources/views/layouts/master.blade.php new file mode 100644 index 00000000..61dea881 --- /dev/null +++ b/resources/views/layouts/master.blade.php @@ -0,0 +1,39 @@ + + + OpenStackId - Resource Server - @yield('title') + + + + +
+ @yield('content') +
+ + \ No newline at end of file diff --git a/resources/views/vendor/.gitkeep b/resources/views/vendor/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/server.php b/server.php new file mode 100644 index 00000000..c7e378df --- /dev/null +++ b/server.php @@ -0,0 +1,21 @@ + + */ + +$uri = urldecode( + parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) +); + +// This file allows us to emulate Apache's "mod_rewrite" functionality from the +// built-in PHP web server. This provides a convenient way to test a Laravel +// application without having installed a "real" web server software here. +if ($uri !== '/' && file_exists(__DIR__.'/public'.$uri)) +{ + return false; +} + +require_once __DIR__.'/public/index.php'; diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..dfea3492 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,23 @@ +[metadata] +name = infra-specs +summary = OpenStackID Resource Server for the OpenStack Foundation site +description-file = + readme.md +author = OpenStack +author-email = openstack-infra@lists.openstack.org +home-page = http://www.openstack.org/ +classifier = + Environment :: OpenStack + Intended Audience :: Developers + Operating System :: POSIX :: Linux + +[build_sphinx] +source-dir = doc/source +build-dir = doc/build +all_files = 1 + +[pbr] +warnerrors = True + +[upload_sphinx] +upload-dir = doc/build/html \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..5229c561 --- /dev/null +++ b/setup.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import setuptools + +setuptools.setup( + setup_requires=['pbr'], + pbr=True) \ No newline at end of file diff --git a/storage/.gitignore b/storage/.gitignore new file mode 100755 index 00000000..78eac7b6 --- /dev/null +++ b/storage/.gitignore @@ -0,0 +1 @@ +laravel.log \ No newline at end of file diff --git a/storage/app/.gitignore b/storage/app/.gitignore new file mode 100755 index 00000000..c96a04f0 --- /dev/null +++ b/storage/app/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/storage/framework/.gitignore b/storage/framework/.gitignore new file mode 100755 index 00000000..1670e906 --- /dev/null +++ b/storage/framework/.gitignore @@ -0,0 +1,6 @@ +config.php +routes.php +compiled.php +services.json +events.scanned.php +routes.scanned.php diff --git a/storage/framework/cache/.gitignore b/storage/framework/cache/.gitignore new file mode 100755 index 00000000..c96a04f0 --- /dev/null +++ b/storage/framework/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/storage/framework/sessions/.gitignore b/storage/framework/sessions/.gitignore new file mode 100755 index 00000000..d6b7ef32 --- /dev/null +++ b/storage/framework/sessions/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/framework/views/.gitignore b/storage/framework/views/.gitignore new file mode 100755 index 00000000..d6b7ef32 --- /dev/null +++ b/storage/framework/views/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/logs/.gitignore b/storage/logs/.gitignore new file mode 100755 index 00000000..d6b7ef32 --- /dev/null +++ b/storage/logs/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/tests/OAuth2ConsultantApiTest.php b/tests/OAuth2ConsultantApiTest.php new file mode 100644 index 00000000..237a2838 --- /dev/null +++ b/tests/OAuth2ConsultantApiTest.php @@ -0,0 +1,154 @@ + 1 , + 'per_page' => 10, + 'status' => ICompanyServiceRepository::Status_active, + ); + + $headers = array("HTTP_Authorization" => " Bearer " .$this->access_token); + $response = $this->action( + "GET", + "OAuth2ConsultantsApiController@getConsultants", + $params, + array(), + array(), + array(), + $headers + ); + + $content = $response->getContent(); + $consultants = json_decode($content); + + $this->assertResponseStatus(200); + } + + public function testGetConsultantsCORS() + { + + $params = array( + 'page' => 1 , + 'per_page' => 10, + 'status' => ICompanyServiceRepository::Status_active, + ); + + $headers = array( + "HTTP_Authorization" => " Bearer " .$this->access_token, + 'HTTP_Origin' => array('www.test.com'), + 'HTTP_Access-Control-Request-Method'=>'GET', + ); + + $response = $this->action( + "OPTIONS", + "OAuth2ConsultantsApiController@getConsultants", + $params, + array(), + array(), + array(), + $headers + ); + + + $content = $response->getContent(); + $consultants = json_decode($content); + + $this->assertResponseStatus(403); + } + + public function testGetConsultantNotFound() + { + + $params = array( + 'id' => 0 + ); + + $headers = array("HTTP_Authorization" => " Bearer " .$this->access_token); + $response = $this->action( + "GET", + "OAuth2ConsultantsApiController@getConsultant", + $params, + array(), + array(), + array(), + $headers + ); + + $content = $response->getContent(); + $res = json_decode($content); + + $this->assertResponseStatus(404); + } + + public function testGetConsultantFound() + { + + $params = array( + 'id' => 18 + ); + + $headers = array("HTTP_Authorization" => " Bearer " .$this->access_token); + $response = $this->action( + "GET", + "OAuth2ConsultantsApiController@getConsultant", + $params, + array(), + array(), + array(), + $headers + ); + + $content = $response->getContent(); + $res = json_decode($content); + + $this->assertResponseStatus(200); + } + + public function testGetOffices() + { + + $params = array( + 'id' => 19 + ); + + $headers = array("HTTP_Authorization" => " Bearer " .$this->access_token); + $response = $this->action( + "GET", + "OAuth2ConsultantsApiController@getOffices", + $params, + array(), + array(), + array(), + $headers + ); + + $content = $response->getContent(); + $res = json_decode($content); + + $this->assertResponseStatus(200); + + } +} \ No newline at end of file diff --git a/tests/OAuth2PrivateCloudApiTest.php b/tests/OAuth2PrivateCloudApiTest.php new file mode 100644 index 00000000..2163e66b --- /dev/null +++ b/tests/OAuth2PrivateCloudApiTest.php @@ -0,0 +1,123 @@ + 1 , + 'per_page' => 10, + 'status' => ICompanyServiceRepository::Status_active, + ); + + $headers = array("HTTP_Authorization" => " Bearer " .$this->access_token); + $response = $this->action( + "GET", + "OAuth2PrivateCloudApiController@getClouds", + $params, + array(), + array(), + array(), + $headers + ); + + $content = $response->getContent(); + $clouds = json_decode($content); + + $this->assertResponseStatus(200); + } + + public function testGetPrivateCloudNotFound() + { + + $params = array( + 'id' => 0 + ); + + $headers = array("HTTP_Authorization" => " Bearer " .$this->access_token); + $response = $this->action( + "GET", + "OAuth2PrivateCloudApiController@getCloud", + $params, + array(), + array(), + array(), + $headers + ); + + $content = $response->getContent(); + $res = json_decode($content); + + $this->assertResponseStatus(404); + } + + public function testGetPrivateCloudFound() + { + + $params = array( + 'id' => 60 + ); + + $headers = array("HTTP_Authorization" => " Bearer " .$this->access_token); + $response = $this->action( + "GET", + "OAuth2PrivateCloudApiController@getCloud", + $params, + array(), + array(), + array(), + $headers + ); + + + $content = $response->getContent(); + $res = json_decode($content); + + $this->assertResponseStatus(200); + } + + public function testGetDataCenterRegions() + { + + $params = array( + 'id' => 60 + ); + + $headers = array("HTTP_Authorization" => " Bearer " .$this->access_token); + $response = $this->action( + "GET", + "OAuth2PrivateCloudApiController@getCloudDataCenters", + $params, + array(), + array(), + array(), + $headers + ); + + $content = $response->getContent(); + $res = json_decode($content); + + $this->assertResponseStatus(200); + + } +} \ No newline at end of file diff --git a/tests/OAuth2PublicCloudApiTest.php b/tests/OAuth2PublicCloudApiTest.php new file mode 100644 index 00000000..3f5a6fca --- /dev/null +++ b/tests/OAuth2PublicCloudApiTest.php @@ -0,0 +1,121 @@ + 1 , + 'per_page' => 10, + 'status' => ICompanyServiceRepository::Status_active, + ); + + $headers = array("HTTP_Authorization" => " Bearer " .$this->access_token); + + $response = $this->action( + "GET", + "OAuth2PublicCloudApiController@getClouds", + $params, + array(), + array(), + array(), + $headers + ); + + $content = $response->getContent(); + $clouds = json_decode($content); + + $this->assertResponseStatus(200); + } + + public function testGetPublicCloudNotFound() + { + + $params = array( + 'id' => 0 + ); + + $headers = array("HTTP_Authorization" => " Bearer " .$this->access_token); + $response = $this->action( + "GET", + "OAuth2PublicCloudApiController@getCloud", + $params, + array(), + array(), + array(), + $headers + ); + + $content = $response->getContent(); + $res = json_decode($content); + + $this->assertResponseStatus(404); + } + + public function testGetPublicCloudFound() + { + + $params = array( + 'id' => 17 + ); + + $headers = array("HTTP_Authorization" => " Bearer " .$this->access_token); + $response = $this->action( + "GET", + "OAuth2PublicCloudApiController@getCloud", + $params, + array(), + array(), + array(), + $headers + ); + + $content = $response->getContent(); + $res = json_decode($content); + + $this->assertResponseStatus(200); + } + + public function testGetDataCenterRegions() + { + + $params = array( + 'id' => 53 + ); + + $headers = array("HTTP_Authorization" => " Bearer " .$this->access_token); + $response = $this->action( + "GET", + "OAuth2PublicCloudApiController@getCloudDataCenters", + $params, + array(), + array(), + array(), + $headers + ); + + $content = $response->getContent(); + $res = json_decode($content); + $this->assertResponseStatus(200); + + } +} \ No newline at end of file diff --git a/tests/ProtectedApiTest.php b/tests/ProtectedApiTest.php new file mode 100644 index 00000000..36340120 --- /dev/null +++ b/tests/ProtectedApiTest.php @@ -0,0 +1,74 @@ +access_token = '123456789'; + parent::setUp(); + } + + public function tearDown() + { + Mockery::close(); + parent::tearDown(); + } +} \ No newline at end of file diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 00000000..ca3ba9f2 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,47 @@ +make('Illuminate\Contracts\Console\Kernel'); + $app->loadEnvironmentFrom('.env.testing'); + $instance->bootstrap(); + return $app; + } + + public function setUp() + { + parent::setUp(); + $this->redis = Redis::connection(); + $this->redis->flushall(); + $this->prepareForTests(); + } + + protected function prepareForTests() + { + Artisan::call('migrate'); + Mail::pretend(true); + $this->seed('TestSeeder'); + } + + public function tearDown() + { + + parent::tearDown(); + } +} \ No newline at end of file diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..30192fe5 --- /dev/null +++ b/tox.ini @@ -0,0 +1,22 @@ +[tox] +minversion = 1.6 +envlist = docs + +[testenv] +install_command = pip install -U {opts} {packages} +setenv = +VIRTUAL_ENV={envdir} +deps = -r{toxinidir}/requirements.txt + +[testenv:venv] +commands = {posargs} + +[testenv:docs] +commands = python setup.py build_sphinx + +[testenv:spelling] +deps = +-r{toxinidir}/requirements.txt +sphinxcontrib-spelling +PyEnchant +commands = sphinx-build -b spelling doc/source doc/build/spelling \ No newline at end of file