Sign inSign up

frolvlad/ddots-api-server

By frolvlad

Updated about 7 years ago

DDOTS RESTful API Server

Image
0

2.2K

frolvlad/ddots-api-server repository overview

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 '[email protected]' \
        '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.

Tag summary

Content type

Image

Digest

Size

546.8 MB

Last updated

about 7 years ago

docker pull frolvlad/ddots-api-server