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

Dev apitoken #155

Closed
wants to merge 2 commits into from
Closed
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
222 changes: 222 additions & 0 deletions docs/rest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,34 @@ total number of objects while ``next`` and ``previous`` will hold URLs to the
next and previous pages. It's possible to change the number of elements per
page with the ``perpage`` GET parameter, with a limit of 100 elements per page.

API Authentication
------------------

APIToken
~~~~~~~~

To use APIToken authentication scheme you'll need to provide it
in Authorization request headers.
If authentication fails you will get a `401` response and the body
will contain the failure reason.

.. sourcecode:: http

GET /api/1.0/ HTTP/1.1
Accept: application/json
Authorization: APIToken 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b

.. sourcecode:: http

HTTP/1.1 401 UNAUTHORIZED
Vary: Accept
Content-Type: application/json

{
"detail": "Invalid API token"
}


API Reference
-------------

Expand Down Expand Up @@ -572,6 +600,200 @@ Patches

$ curl -s http://patchwork.example.com/api/1.0/patches/42/mbox/ | git am -3

APIToken
~~~~~~~~

If any of these API fails, response body contain just one field
containing the failuare reason:

.. sourcecode:: http

POST /api/1.0/tokens/ HTTP/1.1
Content-Type: application/json
Accept: application/json

{
"name":"Foo"
}

.. sourcecode:: http

HTTP/1.1 200 OK
Content-Type: application/json
Vary: Accept
Allow: GET, POST, HEAD, OPTIONS

{
"name": ["Name exists"]
}

.. http:get:: /api/1.0/apitoken/

List all APITokens.

.. sourcecode:: http

GET /api/1.0/tokens/ HTTP/1.1
Accept: application/json

.. sourcecode:: http

HTTP/1.1 200 OK
Content-Type: application/json
Vary: Accept
Allow: GET, POST, HEAD, OPTIONS

[
{
"id": 1,
"name": "JenkinsKey",
"user": 1,
"state": "active",
"created": "2015-11-17T09:47:04.995296"
},
{
"id": 2,
"name": "Key2",
"user": 1,
"state": "active",
"created": "2015-11-17T09:51:17.414646"
}
]

.. http:get:: /api/1.0/tokens/(int: apitoken_id)/

Get specific APIToken.

.. sourcecode:: http

GET /api/1.0/tokens/2/ HTTP/1.1
Accept: application/json

.. sourcecode:: http

HTTP/1.1 200 OK
Content-Type: application/json
Vary: Accept
Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS

{
"id": 2,
"name": "Key2",
"user": 1,
"state": "active",
"created": "2015-11-17T09:51:17.414646"
}

.. http:post:: /api/1.0/tokens/

Create new APIToken.

.. sourcecode:: http

POST /api/1.0/tokens/ HTTP/1.1
Content-Type: application/json
Accept: application/json

{
"name":"Foo"
}

.. sourcecode:: http

HTTP/1.1 200 OK
Content-Type: application/json
Vary: Accept
Allow: GET, POST, HEAD, OPTIONS

{
"id": 3,
"name": "Foo",
"user": 1,
"state": "active",
"created": "2015-11-17T10:03:36.961686",
"apitoken": "0949c2ba0a8aac546ce9eb33778ea8a1f44a2e85"
}

**Note:** This contain the generated APIToken secret
which is included ONLY in this POST response.
You wont be able to retrieve this APIToken again.

.. http:delete:: /api/1.0/tokens/(int: apitoken_id)/

Delete APIToken.

.. sourcecode:: http

DELETE /api/1.0/tokens/3/ HTTP/1.1
Accept: application/json

.. sourcecode:: http

HTTP/1.1 204 NO CONTENT
Content-Type: application/json
Vary: Accept
Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS

.. http:patch:: /api/1.0/tokens/(int: apitoken_id)/

Modify name and/or state.

.. sourcecode:: http

PATCH /api/1.0/tokens/2/ HTTP/1.1
Content-Type: application/json
Accept: application/json

{
"name":"Foo"
}

.. sourcecode:: http

HTTP/1.1 200 OK
Content-Type: application/json
Vary: Accept
Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS

{
"id": 2,
"name": "Foo",
"user": 1,
"state": "Active",
"created": "2015-11-17T09:51:17.414646"
}

.. http:put:: /api/1.0/tokens/(int: apitoken_id)/

Modify name and state.

.. sourcecode:: http

PUT /api/1.0/tokens/2/ HTTP/1.1
Content-Type: application/json
Accept: application/json

{
"name":"Foo",
"state":"Inactive"
}

.. sourcecode:: http

HTTP/1.1 200 OK
Content-Type: application/json
Vary: Accept
Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS

{
"id": 2,
"name": "Foo",
"user": 1,
"state": "Inactive",
"created": "2015-11-17T09:51:17.414646"
}
**Note:** If state is ommited, default value will be used ("Active").

API Revisions
-------------

Expand Down
10 changes: 7 additions & 3 deletions patchwork/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,13 @@

from patchwork.models import (Project, Person, UserProfile, State, Patch,
Comment, Bundle, Tag, Test, TestResult,
DelegationRule, Series, SeriesRevision)

DelegationRule, Series, SeriesRevision,
APIToken)

class DelegationRuleInline(admin.TabularInline):
model = DelegationRule
fields = ('path', 'user', 'priority')


class ProjectAdmin(admin.ModelAdmin):
list_display = ('name', 'linkname', 'listid', 'listemail')
inlines = [
Expand Down Expand Up @@ -128,3 +127,8 @@ class BundleAdmin(admin.ModelAdmin):
class TagAdmin(admin.ModelAdmin):
list_display = ('name',)
admin.site.register(Tag, TagAdmin)

class APITokenAdmin(admin.ModelAdmin):
list_display = ('name', 'user', 'state')
admin.site.register(APIToken, APITokenAdmin)

71 changes: 71 additions & 0 deletions patchwork/authentication.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Patchwork - automated patch tracking system
# Copyright (C) 2015 Intel Corporation
#
# This file is part of the Patchwork package.
#
# Patchwork is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# Patchwork is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Patchwork; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

from rest_framework.authentication import BaseAuthentication, get_authorization_header
from rest_framework import exceptions
from patchwork.models import APIToken

class APITokenAuthentication(BaseAuthentication):
"""
Simple api token based authentication.

Clients should authenticate by passing the api token key in the "Authorization"
HTTP header, prepended with the string "APIToken ". For example:

Authorization: APIToken 401f7ac837da42b97f613d789819ff93537bee6a
"""

model = APIToken

def authenticate(self, request):
auth = get_authorization_header(request).split()
if not auth or auth[0].lower() != b'apitoken':
return None

if len(auth) == 1:
msg = 'Invalid API token header. No credentials provided.'
raise exceptions.AuthenticationFailed(msg)
elif len(auth) > 2:
msg = 'Invalid API token header. API token string should not contain spaces.'
raise exceptions.AuthenticationFailed(msg)

return self.authenticate_credentials(auth[1])

def authenticate_credentials(self, apitoken):
apitoken = self.model.authenticate(apitoken)

if apitoken is None:
msg = 'Invalid API token'
raise exceptions.AuthenticationFailed(msg)

is_active, state = apitoken.get_state()

if not is_active:
msg = 'Inactive APIToken: {}'.format(state)
raise exceptions.AuthenticationFailed(msg)

if not apitoken.user.is_active:
msg = 'User inactive or deleted'
raise exceptions.AuthenticationFailed(msg)

return (apitoken.user, apitoken)

def authenticate_header(self, request):
return 'APIToken'

31 changes: 31 additions & 0 deletions patchwork/migrations/0016_auto_20160223_2340.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import migrations, models
from django.conf import settings


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('patchwork', '0015_remove_version_n_patches'),
]

operations = [
migrations.CreateModel(
name='APIToken',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(max_length=40)),
('state', models.CharField(default=b'active', max_length=20)),
('secret', models.CharField(unique=True, max_length=80, db_index=True)),
('created', models.DateTimeField(auto_now_add=True)),
('user', models.ForeignKey(related_name='auth_token', to=settings.AUTH_USER_MODEL)),
],
),
migrations.AlterUniqueTogether(
name='apitoken',
unique_together=set([('name', 'user')]),
),
]
Loading