Public | Automated Build

Last pushed: 3 months ago
Short Description
DDOTS RESTful API Server
Full Description

DDOTS RESTful API Server

This project aims to provide RESTful (HTTP) API for DDOTS (Dockerized
Distributed Olympiad Testing System).

We implement OpenAPI specification to document the API. OpenAPI enables us to
provide an interactive documentation effortlessly (so run the server and
proceed to http://127.0.0.1:5000/api/v1/), generate client libraries for a
number of programming languages (Python and JavaScript clients are generated,
published to the repositories, and extensively used).

Clone the Project

$ git clone --recurse-submodules https://github.com/frol/ddots-api-server.git

Setup Environment

It is recommended to use Docker, virtualenv, or Anaconda/Miniconda to manage
Python dependencies. Please, learn details yourself. Docker image will be
probably available later.

You will need invoke package to work with everything related to this project.

$ pip install invoke

Once you have invoke, you can learn all available commands related to this
project from:

$ invoke --list

Invoke tasks will prepare automatically everything that is required once you
run the application server.

Run Server

$ invoke app.run

Run SeaweedFS

DDOTS API Server uses SeaweedFS to
store tests for problems and solution source codes. Thus, SeaweedFS is
required.

You may run SeaweedFS locally following
the official documentation,
or you may use Docker containers, in which case you may use Docker Compose
(its config is in deploy/docker-compose.yml).

Run DDOTS with SeaweedFS in Docker Containers

You should have Docker and Docker Compose installed on your computer. Once that
is in place, run the following commands:

$ cd ./deploy
$ docker-compose up --build

You will need to edit deploy/docker-compose.yml if you want to enable
persistent storage (otherwise, updating/recreating Docker containers will wipe
all the data). Follow the instructions in the docker-compose.yml file.

Quickstart

Open online interactive API documentation:
http://127.0.0.1:5000/api/v1/

Autogenerated swagger config is always available from
http://127.0.0.1:5000/api/v1/swagger.json

Typical Workflow

The key components of DDOTS API Server are Solutions and Problems. However,
authentication is the first thing you want to learn before getting started with
the API itself.

Authentication

DDOTS API Server implements OAuth2 flows to enable sane and reusable
authentication protocol.

Currently, there are implemented two OAuth2 flows:

Learn more about OAuth2 authentication by cURL examples
here.

The built-in Swagger UI documentation (http://127.0.0.1:5000/api/v1/) uses
OAuth2 authentication by password and there are four default users:

  • internal with password q (it has INTERNAL role in the permissions system,
    and the system trusts this user to fetch solutions for testing and trusts the
    received reports)
  • root with password q (it has ADMIN role in the permissions system, and
    it is allowed to change passwords, view other users info, etc)
  • user with password w (it is just a regular user)
  • documentation (this user is inactive, it is only a helper user for the
    OAuth2 Resource Owner Password Credentials Grant)

Now that you are aware of the available options, here is what you can do:

Register a new user

By default, any user can sign up into the system on his/her own using the
POST /users/ endpoint. The only trick is the recaptcha_key, which is not
implemented yet and just requires to send secret_key as the value.

Register a new OAuth2 client (application)

OAuth2 client usually refers to an application/script. Think that you could use
username and password saved in plain text in your application config file, but
instead, since you don't want to change your password every time these
credentials leak, you create subcredentials (client_id + client_secret) for
each application instance or at least one set for all the instances of one
application. This way you can revoke the subcredentials and the leaked
credentials will simply stop working. Also, specifying a list of enabled scopes
you can control which parts of the API are allowed to access; for example, your
application only need to access the list of your teams, so you can restrict the
scopes to teams:read or teams:read teams:write. Also, separate credentials
enables fine tracking of which application/service/instance gets access to the
server.

You can register new OAuth2 clients using the direct calls to the internals
(SQLAlchemy models):

from app import create_app
from app.extensions import api, db
from app.modules.auth.models import OAuth2Client
from werkzeug import security

with create_app().app_context():
    new_oauth2_client = OAuth2Client(
        client_id=security.gen_salt(40),
        client_secret=security.gen_salt(50),
        user_id=1,
        redirect_uris=[],
        default_scopes=api.api_v1.authorizations['oauth2_password']['scopes']
    )
    with db.session.begin():
        db.session.add(new_oauth2_client)

    print("New OAuth2 client:")
    print("  CLIENT ID:", new_oauth2_client.client_id)
    print("  CLIENT SECRET:", new_oauth2_client.client_secret)

However, you might be more interested to do that via the RESTful API which does
not require the access to the internals, and you can do this remotely (DDOTS API
is implemented via HTTP protocol).

To register a new OAuth2 client, you should be authenticated into the user,
which you want to receive the client_id and client_secret for and have the
auth:write scope enabled (you may use any of the available authentication
flows which allows you to use the auth:write scope).

Once you authenticated, you can use the POST /auth/oauth2_clients/ endpoint to
create a new set of OAuth2 client credentials. It only requires to specify the
default scopes which will be used as a "maximum allowed scopes" when someone
authenticates using these credentials (note, you specify the list of scopes when
authenticate).

There are three ways to register a new OAuth2 client ("create new
subcredentials") via API:

  • Using the built-in Swagger UI (http://127.0.0.1:5000/api/v1/), navigate to
    "Authentication" -> "Create a new OAuth2 Client", and fill the form (don't
    forget to authenticate using the above usernames and passwords)
  • Using a generated API client (Python, pip install ddots-client):

      import ddots_client
    
      config = ddots_client.Configuration()
      config.host = 'http://127.0.0.1:5000/api/v1'
      config.oauth2_url = 'http://127.0.0.1:5000/auth/oauth2'
    
      import os; os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
      config.get_oauth2_token(client_id='internal', client_secret='Dc5NmZmMjIzODVlZDAwNjQxNmE1NDNkMmYxNjI4N2MwY2E0NjhiMzN')
    
      auth_api = ddots_client.AuthApi(ddots_client.ApiClient(config))
    
      print("New OAuth2 Client response:")
      print(auth_api.create_oauth_client(default_scopes=['solutions:read', 'solutions:write', 'problems:read']))
    
  • Using cURL:

      ACCESS_TOKEN="$(curl --silent 'http://127.0.0.1:5000/auth/oauth2/token?grant_type=client_credentials' --user 'internal:Dc5NmZmMjIzODVlZDAwNjQxNmE1NDNkMmYxNjI4N2MwY2E0NjhiMzN' | sed -E 's/.*access_token": "([^"]+).*/\1/')"
      curl -X POST \
          --header 'Accept: application/json' \
          --header "Authorization: Bearer $ACCESS_TOKEN" \
          -F 'default_scopes=solutions:read' \
          -F 'default_scopes=solutions:write' \
          -F 'default_scopes=problems:read' \
          'http://127.0.0.1:5000/api/v1/auth/oauth2_clients/'
    

The expected output is JSON of the following structure:

{
    "user_id": 1,
    "client_id": "jr0GVDUgwgVo5T81JAetPFt384WRr8mlD3j598tD",
    "client_secret": "T38dYqLqqf1BVS7CmwABsNGkYAVEf4dEYTiueNQQLCiJM4dwdv",
    "client_type": "public",
    "default_scopes": [
        "solutions:read",
        "solutions:write",
        "problems:read"
    ],
    "redirect_uris": []
}

Problems

The first thing you want to do is to create a new Problem. To do that, you need
to use the POST /problems/ endpoint and provide a title and .tar.gz archive
with tests. The .tar.gz archive MUST contain Problem.xml file in the root
and all the related files referenced from the Problem.xml, e.g.:

<?xml version="1.0" encoding="windows-1251"?>
<!-- Problem exchange format 0.1 -->
<Problem
   TimeLimit="0.2"
   MemoryLimit="64"
   InputFile="stdin"
   OutputFile="stdout"
   PatcherExe="patch"
   CheckerExe="check"
   TestCount="2"
   PointsOnGold="100">
    <Test Input="1.in" Answer="1.out" Points="1" />
    <Test Input="2.in" Answer="2.out" Points="1" />
</Problem>

Notes:

  • TimeLimit is specified in seconds (float type value).
  • MemoryLimit is specified in megabytes (integer).
  • InputFile / OutputFile are names of input/output files or special
    stdin/stdout consts can be used.
  • PatcherExe is optional, and its purpose is to patch the user solution on
    the compilation step of the fly (the solution that is being tested is sent to
    the patcher stdin, and the new [potentially patched] solution is expected
    from the patcher stdout).
  • CheckerExe should have a permission to be executed and it might be either a
    statically linked binary or
    Bourne shell script, which
    would accept 3 parameters: test input file, solution answer file, and correct
    test answer file, and exit with exit code 0 if OK, 1 if WA (Wrong
    Answer), 2 if PE (Presentation Error).

You may want to download an example problem tests archive:
example-problem-tests-archive.tar.gz

Once you have the tests archive, you can register a new problem in the system.
There are four ways to achive that:

  • Using DDOTS API Server internals:

      from app import create_app
      from app.extensions import db
      from app.modules.problems.model import Problem
    
      with create_app().app_context():
          new_problem = Problem(
              creator_id=1,
              title="New Problem",
              tests_archive=open('problem-tests-archive.tar.gz')
          )
          with db.session.begin():
              db.session.add(new_problem)
    
          print("New Problem ID is %d" % new_problem.id)
    
  • Using the built-in Swagger UI (http://127.0.0.1:5000/api/v1/), navigate to
    "Problems" -> "Create a new problem", and fill the form (don't forget to
    authenticate)
  • Using a generated API client (Python, pip install ddots-client):

      import ddots_client
    
      config = ddots_client.Configuration()
      config.host = 'http://127.0.0.1:5000/api/v1'
      config.oauth2_url = 'http://127.0.0.1:5000/auth/oauth2'
    
      import os; os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
      config.get_oauth2_token(client_id='internal', client_secret='Dc5NmZmMjIzODVlZDAwNjQxNmE1NDNkMmYxNjI4N2MwY2E0NjhiMzN')
    
      problems_api = ddots_client.ProblemsApi(ddots_client.ApiClient(config))
    
      print("New Problem response:")
      print(problems_api.create_problem(title="New Problem", tests_archive='problem-tests-archive.tar.gz'))
    
  • Using cURL:

      ACCESS_TOKEN="$(curl --silent 'http://127.0.0.1:5000/auth/oauth2/token?grant_type=client_credentials' --user 'internal:Dc5NmZmMjIzODVlZDAwNjQxNmE1NDNkMmYxNjI4N2MwY2E0NjhiMzN' | sed -E 's/.*access_token": "([^"]+).*/\1/')"
      curl -X POST \
          --header 'Accept: application/json' \
          --header "Authorization: Bearer $ACCESS_TOKEN" \
          -F 'title=New Problem' \
          -F 'tests_archive=@problem-tests-archive.tar.gz' \
          'http://127.0.0.1:5000/api/v1/problems/'
    

The expected output is JSON of the following structure:

{
    "id": 1,
    "title": "New Problem",
    "description": "",
    "created": "2017-06-04T08:30:51.582997+00:00",
    "updated": "2017-06-04T08:30:51.583019+00:00"
}

Solutions

NOTE: You will see a use of programming_language_name below. Currently, they
are hardcoded on the Testing System, so you have to use the registered names;
there is a list of them in
migrations/initial_delevelopment_data.py.

Similarly to the Problems API, you can add new solutions into the system:

  • Using direct interaction with internals (SQLAlchemy models):

      from app import create_app
      from app.extensions import db
      from app.modules.solutions.model import Solution
    
      with create_app().app_context():
          new_solution = Solution(
              author_id=1,
              problem_id=1,
              programming_language_name='delphi-fpc',  # Delphi
              testing_mode='full',  # Other options: "one", "first_fail"
              source_code="""begin writeln('Hello World!'); end."""
          )
          with db.session.begin():
              db.session.add(new_solution)
    
          print("New Solution ID is %d" % new_solution.id)
    
  • Using the built-in Swagger UI (http://127.0.0.1:5000/api/v1/), navigate to
    "Solutions" -> "Upload a new solution", and fill the form (don't forget to
    authenticate)
  • Using a generated API client (Python, pip install ddots-client):

      import ddots_client
    
      config = ddots_client.Configuration()
      config.host = 'http://127.0.0.1:5000/api/v1'
      config.oauth2_url = 'http://127.0.0.1:5000/auth/oauth2'
    
      import os; os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
      config.get_oauth2_token(client_id='internal', client_secret='Dc5NmZmMjIzODVlZDAwNjQxNmE1NDNkMmYxNjI4N2MwY2E0NjhiMzN')
    
      solutions_api = ddots_client.SolutionsApi(ddots_client.ApiClient(config))
    
      print("New Solution response:")
      print(solutions_api.send_solution(
          problem_id=1,
          programming_language_name='delphi-fpc',  # Delphi
          testing_mode='full',  # Other options: "one", "first_fail"
          source_code="""begin writeln('Hello World!'); end."""
      ))
    
  • Using cURL:

      ACCESS_TOKEN="$(curl --silent 'http://127.0.0.1:5000/auth/oauth2/token?grant_type=client_credentials' --user 'internal:Dc5NmZmMjIzODVlZDAwNjQxNmE1NDNkMmYxNjI4N2MwY2E0NjhiMzN' | sed -E 's/.*access_token": "([^"]+).*/\1/')"
      curl -X POST \
          --header 'Accept: application/json' \
          --header "Authorization: Bearer $ACCESS_TOKEN" \
          -F 'problem_id=1' \
          -F 'programming_language_name=delphi-fpc' \
          -F 'testing_mode=full' \
          -F "source_code=begin writeln('Hello World!'); end." \
          'http://127.0.0.1:5000/api/v1/solutions/'
    

The expected output is JSON of the following structure:

{
    "id": 1,
    "state": "new",
    "author": {
        "first_name": "",
        "id": 1,
        "last_name": "",
        "middle_name": "",
        "username": "internal"
    },
    "problem": {
        "id": 1,
        "title": "New Problem"
    },
    "programming_language": {
        "name": "delphi-fpc",
        "title": "Delphi / FreePascal",
        "version": ""
    },
    "testing_mode": "full",
    "scored_points": "0.000",
    "status": [],
    "testing_report": null,
    "created": "2017-06-04T08:57:04.634943+00:00",
    "updated": "2017-06-04T08:57:04.634965+00:00"
}

Use the id field to track the solution state and get the testing_report
once state changes to tested.

curl \
    --header 'Accept: application/json' \
    --header "Authorization: Bearer $ACCESS_TOKEN" \
    'http://127.0.0.1:5000/api/v1/solutions/1'
{
    "id": 1,
    "state": "tested",
    "author": {
        "first_name": "",
        "id": 1,
        "last_name": "",
        "middle_name": "",
        "username": "internal"
    },
    "problem": {
        "id": 1,
        "title": "New Problem"
    },
    "programming_language": {
        "name": "delphi-fpc",
        "title": "Delphi / FreePascal",
        "version": ""
    },
    "scored_points": "100.000",
    "status": ["OK"],
    "testing_mode": "full",
    "testing_report": {
        "tests": [
            {
                "execution_time": 0.0,
                "extra": {
                    "scored_points": 1
                },
                "memory_peak": 212992.0,
                "status": "OK"
            },
            {
                "execution_time": 0.0,
                "extra": {
                    "scored_points": 99.0
                },
                "memory_peak": 0.0,
                "status": "OK"
            }
        ]
    },
    "created": "2017-06-04T08:57:04.634943+00:00",
    "updated": "2017-06-04T08:57:06.641482+00:00"
}

NOTE: When tests fail with WA or PE status, extra section contains additional
fields:

  • input — test input file contents;
  • answer — test answer file contents;
  • output — solution output file (or stdout) contents.

Generate API Client

There are Python and JavaScript API clients configured and generated
automatically, but you may want to generate more clients, e.g. for C#, Bash,
Java, Go, PHP, Ruby, etc.
Thus, you may simply create a new folder in clients/ with the name of the
generator (e.g. bash), and put swagger_codegen_config.json in it (learn
more about available config options by examples of the currently configured
clients, and from
the official usage documentation).
Once the configs are in place, run the following command:

$ invoke app.swagger.codegen --language "language_name"

It will produce a dist folder in the clients/language_name/ folder. It
should be enough to get the module working. Each language generator is unique
in their support features, and OAuth2 support is usually missing, so keep that
in mind; we use
a patched version of Swagger Codegen
for Python and JavaScript to enable OAuth2 support.

Docker Pull Command
Owner
frolvlad
Source Repository

Comments (0)