Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API-2.5: documentation and specs #577

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,7 @@ group :test do
# code coverage
gem 'simplecov', require: false
gem 'coveralls', require: false
# api
gem 'apivore', require: false
gem 'hashie', '~> 3.4.6', require: false # https://github.com/westfieldlabs/apivore/issues/114
end
12 changes: 12 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,13 @@ GEM
activerecord (>= 3.0.0)
addressable (2.5.2)
public_suffix (>= 2.0.2, < 4.0)
apivore (1.6.2)
actionpack (>= 4, < 6)
hashie (~> 3.3)
json-schema (~> 2.5)
rspec (~> 3)
rspec-expectations (~> 3.1)
rspec-mocks (~> 3.1)
arel (6.0.4)
attribute_normalizer (1.2.0)
base32 (0.3.2)
Expand Down Expand Up @@ -191,6 +198,7 @@ GEM
has_scope (0.7.2)
actionpack (>= 4.1)
activesupport (>= 4.1)
hashie (3.4.6)
html2haml (2.2.0)
erubis (~> 2.7.0)
haml (>= 4.0, < 6)
Expand All @@ -217,6 +225,8 @@ GEM
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (2.1.0)
json-schema (2.8.0)
addressable (>= 2.4)
jsonapi-renderer (0.2.0)
kaminari (1.1.1)
activesupport (>= 4.1.0)
Expand Down Expand Up @@ -499,6 +509,7 @@ DEPENDENCIES
active_model_serializers (~> 0.10.0)
acts_as_tree
acts_as_versioned!
apivore
attribute_normalizer
better_errors
binding_of_caller
Expand All @@ -523,6 +534,7 @@ DEPENDENCIES
gaffe
haml (~> 4.0)
haml-rails
hashie (~> 3.4.6)
i18n-js (~> 3.0.0.rc8)
i18n-spec
ice_cube
Expand Down
5 changes: 5 additions & 0 deletions db/migrate/20181013195028_add_confidential_to_applications.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddConfidentialToApplications < ActiveRecord::Migration
def change
add_column :oauth_applications, :confidential, :boolean, null: false, default: true
end
end
11 changes: 6 additions & 5 deletions db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -291,13 +291,14 @@
add_index "oauth_access_tokens", ["token"], name: "index_oauth_access_tokens_on_token", unique: true, using: :btree

create_table "oauth_applications", force: :cascade do |t|
t.string "name", limit: 255, null: false
t.string "uid", limit: 255, null: false
t.string "secret", limit: 255, null: false
t.text "redirect_uri", limit: 65535, null: false
t.string "scopes", limit: 255, default: "", null: false
t.string "name", limit: 255, null: false
t.string "uid", limit: 255, null: false
t.string "secret", limit: 255, null: false
t.text "redirect_uri", limit: 65535, null: false
t.string "scopes", limit: 255, default: "", null: false
t.datetime "created_at"
t.datetime "updated_at"
t.boolean "confidential", default: true, null: false
end

add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree
Expand Down
92 changes: 92 additions & 0 deletions doc/API.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Foodsoft API

Foodsoft provides a JSON REST API that gives access to operations like
like listing open orders, updating the ordergroup's order, and listing financial
transactions. Not all Foodsoft functionality is available through the API, but
we're open for new additions.

The API is documented using [Open API 2.0](https://github.com/OAI/OpenAPI-Specification)
/ [Swagger](https://swagger.io/) in [swagger.v1.yml](swagger.v1.yml).
This provides a machine-readable reference that is used to provide documentation.

## API endpoint documentation

&gt;&gt; [View API documentation](http://petstore.swagger.io/?url=https%3A%2F%2Fraw.githubusercontent.com%2Ffoodcoops%2Ffoodsoft%2Fmaster%2Fdoc%2Fswagger.v1.yml) &lt;&lt;

The above documentation can communicate with the API directly on a local development
installation of Foodsoft at [http://localhost:3000/f](http://localhost:3000/f).

You'll need to give access to the application first. This can be done by going to
_Administration_ > _Configuration_ > _Apps_ in Foodsoft. Select _New Application_,
enter any name, put `http://petstore.swagger.io/oauth2-redirect.html` in _Redirect URI_
and disable _Confidential_. After submission, you will have an _Application UID_ that
you can enter that as `client_id` after clicking _Authorize_ in the Swagger UI.


## Security

Uses the [Doorkeeper](https://github.com/doorkeeper-gem/doorkeeper) gem,
which provides an OAuth2 provider.


### Authorization code flow

This is the recommended flow for server-side web applications, where
members login with Foodsoft, then redirected to the app, which then obtains
an access token using the authorization code supplied at redirection.

Before you can obtain an access token, the client needs to obtain an id and secret.
(You can currently skip this for the password credentials flow.) This needs to be
done for each Foodsoft scope by an admin.

1. Click on the _Apps_ button at the right in Foodsoft's configuration screen.
2. Click on _New application_
3. Enter any _Name_ and put the website of your app in _Redirect URI_ and _Submit_.
4. Click on the new applications' name for the app id and secret.
5. To quickly test, logging into the app, press _Authorize_.

Note that the user doesn't need to confirm that he is giving the app access to his
Foodsoft account by default, since apps can only be created by admins. If you
want to change that, see disable `skip_authorization` in `config/initializers/doorkeeper.rb`.

[Read more](https://github.com/doorkeeper-gem/doorkeeper/wiki/Authorization-Code-Flow).


### Implicit flow

This is the recommended flow for client-side web applications. It looks a lot
like the authorization code flow, but when redirecting back to the app, the
access token is available directly as part of the url _fragment_ (`window.location.hash`).

This flow also needs to be registered in Foodsoft as in the authorization code flow,
but with _Confidential_ disabled. You only need the `client_id`, not the secret.

**note** please make sure you understand sections
[4.4.2](http://tools.ietf.org/html/rfc6819#section-4.4.2) and
[4.4.3](http://tools.ietf.org/html/rfc6819#section-4.4.3) of the OAuth2 Threat
Model document before using this flow.

You may find Doorkeeper's [implicit_grant_test](https://github.com/doorkeeper-gem/doorkeeper/blob/master/spec/requests/flows/implicit_grant_spec.rb) useful.


### Password credentials flow

To obtain a token using a username/password directly, you can do this:

```ruby
require 'oauth2'
c = OAuth2::Client.new('client_id', 'secret', site: 'http://localhost:3002/f/', authorize_url: 'oauth/authorize', token_url: 'oauth/token')
c.password.get_token('admin', 'secret').token
# => "1234567890abcdef1234567890abcdef1234567890abcdef123456790abcdef1"
```

Now use this token as value for the `access_token` when accessing the API, like
http://localhost:3002/f/api/v1/financial_transactions/1?access_token=12345...

[Read more](https://github.com/doorkeeper-gem/doorkeeper/wiki/Client-Credentials-flow).


## Logout

When the user logs out of Foodsoft, all access tokens are destroyed, except when
the token's scope includes `offline_access` (so offline applications are possible).
153 changes: 153 additions & 0 deletions doc/swagger.v1.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
swagger: '2.0'
info:
title: Foodsoft API v1
version: '1.0.0'
description: >
[Foodsoft](https://github.com/foodcoops/foodsoft) is web-based software to manage
a non-profit food coop (product catalog, ordering, accounting, job scheduling).


This is a description of Foodsoft's API v1.


Note that each food cooperative typically has their own instance (on a shared
server or their own installation), and there are just as many APIs (if the Foodsoft
version is recent enough).
This API description points to the default development url with the default
Foodsoft scope - that would be [http://localhost:3000/f](http://localhost:3000/f).
externalDocs:
description: General Foodsoft API documentation
url: https://github.com/foodcoops/foodsoft/blob/master/doc/API.md

# development url with default scope
host: localhost:3000
schemes:
- 'http'
basePath: /f/api/v1

produces:
- 'application/json'

paths:
/user:
get:
summary: info about the currently logged-in user
tags:
- 1. User
responses:
200:
description: success
schema:
type: object
properties:
user:
$ref: '#/definitions/User'
401:
description: not logged-in
schema:
$ref: '#/definitions/Error401'
security:
- foodsoft_auth: ['all']
/config:
get:
summary: configuration variables
tags:
- 7. General
responses:
200:
description: success
schema:
type: object
wvengen marked this conversation as resolved.
Show resolved Hide resolved
401:
description: not logged-in
schema:
$ref: '#/definitions/Error401'
security:
- foodsoft_auth: ['all']
/navigation:
get:
summary: navigation
tags:
- 7. General
responses:
200:
description: success
schema:
type: object
wvengen marked this conversation as resolved.
Show resolved Hide resolved
properties:
navigation:
$ref: '#/definitions/Navigation'
401:
description: not logged-in
schema:
$ref: '#/definitions/Error401'
security:
- foodsoft_auth: ['all']

definitions:
# models
User:
type: object
properties:
id:
type: integer
name:
type: string
description: full name
email:
type: string
description: email address
locale:
type: string
description: language code
required: ['id', 'name', 'email']
Navigation:
type: array
items:
type: object
properties:
name:
type: string
description: title
url:
type: string
description: link
items:
$ref: '#/definitions/Navigation'
required: ['name']
minProperties: 2 # name+url or name+items

Error:
type: object
properties:
error:
type: string
description: error code
error_description:
type: string
description: human-readable error message (localized)
Error404:
type: object
properties:
error:
type: string
description: '<tt>not_found</tt>'
error_description:
$ref: '#/definitions/Error/properties/error_description'
Error401:
type: object
properties:
error:
type: string
description: '<tt>unauthorized</tt>'
error_description:
$ref: '#/definitions/Error/properties/error_description'

securityDefinitions:
foodsoft_auth:
type: oauth2
flow: implicit
authorizationUrl: http://localhost:3000/f/oauth/authorize
scopes:
all: full access to user functions
offline_access: retain access after user has logged out
49 changes: 49 additions & 0 deletions spec/api/v1/swagger_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
require 'spec_helper'
require 'apivore'

# we want to load a local file in YAML-format instead of a served JSON file
class SwaggerCheckerFile < Apivore::SwaggerChecker
def fetch_swagger!
YAML.load(File.read(swagger_path))
end
end

describe 'API v1', type: :apivore, order: :defined do
include ApiHelper

subject { SwaggerCheckerFile.instance_for Rails.root.join('doc', 'swagger.v1.yml') }

context 'has valid paths' do
context 'user' do
# create multiple users to make sure we're getting the authenticated user, not just any
let!(:other_user_1) { create :user }
let!(:user) { create :user }
let!(:other_user_2) { create :user }

it { is_expected.to validate(:get, '/user', 200, api_auth) }
it { is_expected.to validate(:get, '/user', 401) }

context 'with invalid access token' do
let(:api_access_token) { 'abc' }
it { is_expected.to validate(:get, '/user', 401, api_auth) }
end
end

context 'config' do
it { is_expected.to validate(:get, '/config', 200, api_auth) }
it { is_expected.to validate(:get, '/config', 401) }
end

context 'navigation' do
it { is_expected.to validate(:get, '/navigation', 200, api_auth) }
it { is_expected.to validate(:get, '/navigation', 401) }
end
end

# needs to be last context so it is always run at the end
context 'and finally' do
it 'tests all documented routes' do
is_expected.to validate_all_paths
end
end
end
15 changes: 15 additions & 0 deletions spec/factories/doorkeeper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
require 'factory_bot'
require 'doorkeeper'

FactoryBot.define do

factory :oauth2_application, class: Doorkeeper::Application do
name { Faker::App.name }
redirect_uri 'https://example.com:1234/app'
end

factory :oauth2_access_token, class: Doorkeeper::AccessToken do
application factory: :oauth2_application
end

end
Loading