Skip to content

Commit

Permalink
Merge pull request deis#4193 from Joshua-Anderson/app-transfer
Browse files Browse the repository at this point in the history
feat(controller): allow users to transfer app ownership.
  • Loading branch information
mboersma committed Sep 9, 2015
2 parents 5878b8f + 86c12d9 commit 418b998
Show file tree
Hide file tree
Showing 17 changed files with 1,693 additions and 9 deletions.
21 changes: 21 additions & 0 deletions client/cmd/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,3 +240,24 @@ func AppDestroy(appID, confirm string) error {

return nil
}

// AppTransfer transfers app ownership to another user.
func AppTransfer(appID, username string) error {
c, appID, err := load(appID)

if err != nil {
return err
}

fmt.Printf("Transferring %s to %s... ", appID, username)

err = apps.Transfer(c, appID, username)

if err != nil {
return err
}

fmt.Println("done")

return nil
}
5 changes: 5 additions & 0 deletions client/controller/api/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ type AppCreateRequest struct {
ID string `json:"id,omitempty"`
}

// AppUpdateRequest is the definition of POST /v1/apps/<app id>/.
type AppUpdateRequest struct {
Owner string `json:"owner,omitempty"`
}

// AppRunRequest is the definition of POST /v1/apps/<app id>/run.
type AppRunRequest struct {
Command string `json:"command"`
Expand Down
15 changes: 15 additions & 0 deletions client/controller/models/apps/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,18 @@ func Delete(c *client.Client, appID string) error {
_, err := c.BasicRequest("DELETE", u, nil)
return err
}

// Transfer an app to another user.
func Transfer(c *client.Client, appID string, username string) error {
u := fmt.Sprintf("/v1/apps/%s/", appID)

req := api.AppUpdateRequest{Owner: username}
body, err := json.Marshal(req)

if err != nil {
return err
}

_, err = c.BasicRequest("POST", u, body)
return err
}
44 changes: 44 additions & 0 deletions client/controller/models/apps/apps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const appsFixture string = `

const appCreateExpected string = `{"id":"example-go"}`
const appRunExpected string = `{"command":"echo hi"}`
const appTransferExpected string = `{"owner":"test"}`

type fakeHTTPServer struct {
createID bool
Expand Down Expand Up @@ -127,6 +128,27 @@ func (f *fakeHTTPServer) ServeHTTP(res http.ResponseWriter, req *http.Request) {
return
}

if req.URL.Path == "/v1/apps/example-go/" && req.Method == "POST" {
body, err := ioutil.ReadAll(req.Body)

if err != nil {
fmt.Println(err)
res.WriteHeader(http.StatusInternalServerError)
res.Write(nil)
}

if string(body) != appTransferExpected {
fmt.Printf("Expected '%s', Got '%s'\n", appTransferExpected, body)
res.WriteHeader(http.StatusInternalServerError)
res.Write(nil)
return
}

res.WriteHeader(http.StatusNoContent)
res.Write(nil)
return
}

fmt.Printf("Unrecongized URL %s\n", req.URL)
res.WriteHeader(http.StatusNotFound)
res.Write(nil)
Expand Down Expand Up @@ -347,3 +369,25 @@ func TestAppsLogs(t *testing.T) {
}
}
}

func TestAppsTransfer(t *testing.T) {
t.Parallel()

handler := fakeHTTPServer{}
server := httptest.NewServer(&handler)
defer server.Close()

u, err := url.Parse(server.URL)

if err != nil {
t.Fatal(err)
}

httpClient := client.CreateHTTPClient(false)

client := client.Client{HTTPClient: httpClient, ControllerURL: *u, Token: "abc"}

if err = Transfer(&client, "example-go", "test"); err != nil {
t.Fatal(err)
}
}
28 changes: 27 additions & 1 deletion client/parser/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
docopt "github.com/docopt/docopt-go"
)

// Apps routes app commands to the specific function
// Apps routes app commands to their specific function.
func Apps(argv []string) error {
usage := `
Valid commands for apps:
Expand All @@ -20,6 +20,7 @@ apps:open open the application in a browser
apps:logs view aggregated application logs
apps:run run a command in an ephemeral app container
apps:destroy destroy an application
apps:transfer transfer app ownership to another user
Use 'deis help [command]' to learn more.
`
Expand All @@ -39,6 +40,8 @@ Use 'deis help [command]' to learn more.
return appRun(argv)
case "apps:destroy":
return appDestroy(argv)
case "apps:transfer":
return appTransfer(argv)
default:
if printHelp(argv, usage) {
return nil
Expand Down Expand Up @@ -244,3 +247,26 @@ Options:

return cmd.AppDestroy(app, confirm)
}

func appTransfer(argv []string) error {
usage := `
Transfer app ownership to another user.
Usage: deis apps:transfer <username> [options]
Arguments:
<username>
the user that the app will be transfered to.
Options:
-a --app=<app>
the uniquely identifiable name for the application.
`
args, err := docopt.Parse(usage, argv, true, "", false, true)

if err != nil {
return err
}

return cmd.AppTransfer(safeGetValue(args, "--app"), safeGetValue(args, "<username>"))
}
2 changes: 1 addition & 1 deletion controller/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
The **api** Django app presents a RESTful web API for interacting with the **deis** system.
"""

__version__ = '1.6.0'
__version__ = '1.7.0'
18 changes: 18 additions & 0 deletions controller/api/fixtures/tests.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,23 @@
"email": "[email protected]",
"date_joined": "2013-05-10T16:08:09.357Z"
}
},
{
"pk": 9,
"model": "auth.user",
"fields": {
"username": "autotest3",
"first_name": "Otto",
"last_name": "Test",
"is_active": true,
"is_superuser": false,
"is_staff": false,
"last_login": "2013-05-10T16:08:09.357Z",
"groups": [],
"user_permissions": [],
"password": "pbkdf2_sha256$10000$5Uoq7dl61vnN$gQhDpc2q2Rkn16VdPC+pNNEQcKpy+LGe29Zkad+2/m4=",
"email": "[email protected]",
"date_joined": "2013-05-10T16:08:09.357Z"
}
}
]
46 changes: 46 additions & 0 deletions controller/api/tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,52 @@ def test_app_info_not_showing_wrong_app(self):
response = self.client.get(url, HTTP_AUTHORIZATION='token {}'.format(self.token))
self.assertEqual(response.status_code, 404)

def test_app_transfer(self):
owner = User.objects.get(username='autotest2')
owner_token = Token.objects.get(user=owner).key
app_id = 'autotest'
base_url = '/v1/apps'
body = {'id': app_id}
response = self.client.post(base_url, json.dumps(body), content_type='application/json',
HTTP_AUTHORIZATION='token {}'.format(owner_token))
# Transfer App
url = '{}/{}'.format(base_url, app_id)
new_owner = User.objects.get(username='autotest3')
new_owner_token = Token.objects.get(user=new_owner).key
body = {'owner': new_owner.username}
response = self.client.post(url, json.dumps(body), content_type='application/json',
HTTP_AUTHORIZATION='token {}'.format(owner_token))
self.assertEqual(response.status_code, 200)

# Original user can no longer access it
response = self.client.get(url, HTTP_AUTHORIZATION='token {}'.format(owner_token))
self.assertEqual(response.status_code, 403)

# New owner can access it
response = self.client.get(url, HTTP_AUTHORIZATION='token {}'.format(new_owner_token))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['owner'], new_owner.username)

# Collaborators can't transfer
body = {'username': owner.username}
perms_url = url+"/perms/"
response = self.client.post(perms_url, json.dumps(body), content_type='application/json',
HTTP_AUTHORIZATION='token {}'.format(new_owner_token))
self.assertEqual(response.status_code, 201)
body = {'owner': self.user.username}
response = self.client.post(url, json.dumps(body), content_type='application/json',
HTTP_AUTHORIZATION='token {}'.format(owner_token))
self.assertEqual(response.status_code, 403)

# Admins can transfer
body = {'owner': self.user.username}
response = self.client.post(url, json.dumps(body), content_type='application/json',
HTTP_AUTHORIZATION='token {}'.format(self.token))
self.assertEqual(response.status_code, 200)
response = self.client.get(url, HTTP_AUTHORIZATION='token {}'.format(self.token))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['owner'], self.user.username)


FAKE_LOG_DATA = """
2013-08-15 12:41:25 [33454] [INFO] Starting gunicorn 17.5
Expand Down
4 changes: 1 addition & 3 deletions controller/api/tests/test_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ def test_super_user_can_list(self):
HTTP_AUTHORIZATION='token {}'.format(token))

self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data['results']), 2)
self.assertEqual(response.data['results'][0]['username'], 'autotest')
self.assertEqual(response.data['results'][1]['username'], 'autotest2')
self.assertEqual(len(response.data['results']), 3)

def test_non_super_user_cannot_list(self):
url = '/v1/users/'
Expand Down
2 changes: 1 addition & 1 deletion controller/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
views.AppPermsViewSet.as_view({'get': 'list', 'post': 'create'})),
# apps base endpoint
url(r"^apps/(?P<id>{})/?".format(settings.APP_URL_REGEX),
views.AppViewSet.as_view({'get': 'retrieve', 'delete': 'destroy'})),
views.AppViewSet.as_view({'get': 'retrieve', 'post': 'update', 'delete': 'destroy'})),
url(r'^apps/?',
views.AppViewSet.as_view({'get': 'list', 'post': 'create'})),
# key
Expand Down
11 changes: 11 additions & 0 deletions controller/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,17 @@ def run(self, request, **kwargs):
return Response(output_and_rc, status=status.HTTP_200_OK,
content_type='text/plain')

def update(self, request, **kwargs):
app = self.get_object()

if request.data.get('owner'):
if self.request.user != app.owner and not self.request.user.is_superuser:
raise PermissionDenied()
new_owner = get_object_or_404(User, username=request.data['owner'])
app.owner = new_owner
app.save()
return Response(status=status.HTTP_200_OK)


class BuildViewSet(ReleasableViewSet):
"""A viewset for interacting with Build objects."""
Expand Down
3 changes: 1 addition & 2 deletions docs/reference/api-v1.6.rst
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
:title: Controller API v1.6
:description: The v1.6 REST API for Deis' Controller

.. _controller_api_v1:

Controller API v1.6
===================

Expand All @@ -18,6 +16,7 @@ What's New

**New!** ``?page_size`` query parameter for paginated requests to set the number of results per page.


Authentication
--------------

Expand Down
Loading

0 comments on commit 418b998

Please sign in to comment.