Skip to content

Commit e78786e

Browse files
vtemianlafriks
authored andcommitted
Writable deploy keys (closes #671) (#3225)
* Add is_writable checkbox to deploy keys interface * Add writable key option to deploy key form * Add support for writable ssh keys in the interface * Rename IsWritable to ReadOnly * Test: create read-only and read-write deploy keys via api * Add DeployKey access mode migration * Update gitea sdk via govendor * Fix deploykey migration * Add unittests for writable deploy keys * Move template text to locale * Remove implicit column update * Remove duplicate locales * Replace ReadOnly field with IsReadOnly method * Fix deploy_keys related integration test * Rename v54 migration with v55 * Fix migration hell
1 parent 70b6c07 commit e78786e

File tree

13 files changed

+184
-13
lines changed

13 files changed

+184
-13
lines changed

integrations/api_keys_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
package integrations
66

77
import (
8+
"fmt"
89
"net/http"
910
"testing"
1011

12+
"code.gitea.io/gitea/models"
1113
api "code.gitea.io/sdk/gitea"
1214
)
1315

@@ -37,3 +39,54 @@ func TestDeleteDeployKeyNoLogin(t *testing.T) {
3739
req := NewRequest(t, "DELETE", "/api/v1/repos/user2/repo1/keys/1")
3840
MakeRequest(t, req, http.StatusUnauthorized)
3941
}
42+
43+
func TestCreateReadOnlyDeployKey(t *testing.T) {
44+
prepareTestEnv(t)
45+
repo := models.AssertExistsAndLoadBean(t, &models.Repository{Name: "repo1"}).(*models.Repository)
46+
repoOwner := models.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User)
47+
48+
session := loginUser(t, repoOwner.Name)
49+
50+
keysURL := fmt.Sprintf("/api/v1/repos/%s/%s/keys", repoOwner.Name, repo.Name)
51+
rawKeyBody := api.CreateKeyOption{
52+
Title: "read-only",
53+
Key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDAu7tvIvX6ZHrRXuZNfkR3XLHSsuCK9Zn3X58lxBcQzuo5xZgB6vRwwm/QtJuF+zZPtY5hsQILBLmF+BZ5WpKZp1jBeSjH2G7lxet9kbcH+kIVj0tPFEoyKI9wvWqIwC4prx/WVk2wLTJjzBAhyNxfEq7C9CeiX9pQEbEqJfkKCQ== nocomment\n",
54+
ReadOnly: true,
55+
}
56+
req := NewRequestWithJSON(t, "POST", keysURL, rawKeyBody)
57+
resp := session.MakeRequest(t, req, http.StatusCreated)
58+
59+
var newDeployKey api.DeployKey
60+
DecodeJSON(t, resp, &newDeployKey)
61+
models.AssertExistsAndLoadBean(t, &models.DeployKey{
62+
ID: newDeployKey.ID,
63+
Name: rawKeyBody.Title,
64+
Content: rawKeyBody.Key,
65+
Mode: models.AccessModeRead,
66+
})
67+
}
68+
69+
func TestCreateReadWriteDeployKey(t *testing.T) {
70+
prepareTestEnv(t)
71+
repo := models.AssertExistsAndLoadBean(t, &models.Repository{Name: "repo1"}).(*models.Repository)
72+
repoOwner := models.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User)
73+
74+
session := loginUser(t, repoOwner.Name)
75+
76+
keysURL := fmt.Sprintf("/api/v1/repos/%s/%s/keys", repoOwner.Name, repo.Name)
77+
rawKeyBody := api.CreateKeyOption{
78+
Title: "read-write",
79+
Key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDsufOCrDDlT8DLkodnnJtbq7uGflcPae7euTfM+Laq4So+v4WeSV362Rg0O/+Sje1UthrhN6lQkfRkdWIlCRQEXg+LMqr6RhvDfZquE2Xwqv/itlz7LjbdAUdYoO1iH7rMSmYvQh4WEnC/DAacKGbhdGIM/ZBz0z6tHm7bPgbI9ykEKekTmPwQFP1Qebvf5NYOFMWqQ2sCEAI9dBMVLoojsIpV+KADf+BotiIi8yNfTG2rzmzpxBpW9fYjd1Sy1yd4NSUpoPbEJJYJ1TrjiSWlYOVq9Ar8xW1O87i6gBjL/3zN7ANeoYhaAXupdOS6YL22YOK/yC0tJtXwwdh/eSrh",
80+
}
81+
req := NewRequestWithJSON(t, "POST", keysURL, rawKeyBody)
82+
resp := session.MakeRequest(t, req, http.StatusCreated)
83+
84+
var newDeployKey api.DeployKey
85+
DecodeJSON(t, resp, &newDeployKey)
86+
models.AssertExistsAndLoadBean(t, &models.DeployKey{
87+
ID: newDeployKey.ID,
88+
Name: rawKeyBody.Title,
89+
Content: rawKeyBody.Key,
90+
Mode: models.AccessModeWrite,
91+
})
92+
}

models/fixtures/deploy_key.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[] # empty

models/migrations/migrations.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ var migrations = []Migration{
162162
NewMigration("add reactions", addReactions),
163163
// v54 -> v55
164164
NewMigration("add pull request options", addPullRequestOptions),
165+
// v55 -> v56
166+
NewMigration("add writable deploy keys", addModeToDeploKeys),
165167
}
166168

167169
// Migrate database to current version

models/migrations/v55.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright 2018 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package migrations
6+
7+
import (
8+
"fmt"
9+
10+
"code.gitea.io/gitea/models"
11+
"github.com/go-xorm/xorm"
12+
)
13+
14+
func addModeToDeploKeys(x *xorm.Engine) error {
15+
type DeployKey struct {
16+
Mode models.AccessMode `xorm:"NOT NULL DEFAULT 1"`
17+
}
18+
19+
if err := x.Sync2(new(DeployKey)); err != nil {
20+
return fmt.Errorf("Sync2: %v", err)
21+
}
22+
return nil
23+
}

models/ssh_key.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,8 @@ type DeployKey struct {
600600
Fingerprint string
601601
Content string `xorm:"-"`
602602

603+
Mode AccessMode `xorm:"NOT NULL DEFAULT 1"`
604+
603605
CreatedUnix util.TimeStamp `xorm:"created"`
604606
UpdatedUnix util.TimeStamp `xorm:"updated"`
605607
HasRecentActivity bool `xorm:"-"`
@@ -622,6 +624,11 @@ func (key *DeployKey) GetContent() error {
622624
return nil
623625
}
624626

627+
// IsReadOnly checks if the key can only be used for read operations
628+
func (key *DeployKey) IsReadOnly() bool {
629+
return key.Mode == AccessModeRead
630+
}
631+
625632
func checkDeployKey(e Engine, keyID, repoID int64, name string) error {
626633
// Note: We want error detail, not just true or false here.
627634
has, err := e.
@@ -646,7 +653,7 @@ func checkDeployKey(e Engine, keyID, repoID int64, name string) error {
646653
}
647654

648655
// addDeployKey adds new key-repo relation.
649-
func addDeployKey(e *xorm.Session, keyID, repoID int64, name, fingerprint string) (*DeployKey, error) {
656+
func addDeployKey(e *xorm.Session, keyID, repoID int64, name, fingerprint string, mode AccessMode) (*DeployKey, error) {
650657
if err := checkDeployKey(e, keyID, repoID, name); err != nil {
651658
return nil, err
652659
}
@@ -656,6 +663,7 @@ func addDeployKey(e *xorm.Session, keyID, repoID int64, name, fingerprint string
656663
RepoID: repoID,
657664
Name: name,
658665
Fingerprint: fingerprint,
666+
Mode: mode,
659667
}
660668
_, err := e.Insert(key)
661669
return key, err
@@ -670,15 +678,20 @@ func HasDeployKey(keyID, repoID int64) bool {
670678
}
671679

672680
// AddDeployKey add new deploy key to database and authorized_keys file.
673-
func AddDeployKey(repoID int64, name, content string) (*DeployKey, error) {
681+
func AddDeployKey(repoID int64, name, content string, readOnly bool) (*DeployKey, error) {
674682
fingerprint, err := calcFingerprint(content)
675683
if err != nil {
676684
return nil, err
677685
}
678686

687+
accessMode := AccessModeRead
688+
if !readOnly {
689+
accessMode = AccessModeWrite
690+
}
691+
679692
pkey := &PublicKey{
680693
Fingerprint: fingerprint,
681-
Mode: AccessModeRead,
694+
Mode: accessMode,
682695
Type: KeyTypeDeploy,
683696
}
684697
has, err := x.Get(pkey)
@@ -701,7 +714,7 @@ func AddDeployKey(repoID int64, name, content string) (*DeployKey, error) {
701714
}
702715
}
703716

704-
key, err := addDeployKey(sess, pkey.ID, repoID, name, pkey.Fingerprint)
717+
key, err := addDeployKey(sess, pkey.ID, repoID, name, pkey.Fingerprint, accessMode)
705718
if err != nil {
706719
return nil, fmt.Errorf("addDeployKey: %v", err)
707720
}

modules/auth/user_form.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,10 @@ func (f *AddOpenIDForm) Validate(ctx *macaron.Context, errs binding.Errors) bind
169169

170170
// AddKeyForm form for adding SSH/GPG key
171171
type AddKeyForm struct {
172-
Type string `binding:"OmitEmpty"`
173-
Title string `binding:"Required;MaxSize(50)"`
174-
Content string `binding:"Required"`
172+
Type string `binding:"OmitEmpty"`
173+
Title string `binding:"Required;MaxSize(50)"`
174+
Content string `binding:"Required"`
175+
IsWritable bool
175176
}
176177

177178
// Validate validates the fields

options/locale/locale_en-US.ini

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,8 @@ valid_until = Valid until
401401
valid_forever = Valid forever
402402
last_used = Last used on
403403
no_activity = No recent activity
404+
can_read_info = Read
405+
can_write_info = Write
404406
key_state_desc = This key has been used in the last 7 days
405407
token_state_desc = This token has been used in the last 7 days
406408
show_openid = Show on profile
@@ -995,6 +997,8 @@ settings.add_dingtalk_hook_desc = Add <a href="%s">Dingtalk</a> integration to y
995997
settings.deploy_keys = Deploy Keys
996998
settings.add_deploy_key = Add Deploy Key
997999
settings.deploy_key_desc = Deploy keys have read-only access. They are not the same as personal account SSH keys.
1000+
settings.is_writable = Allow write access
1001+
settings.is_writable_info = Can this key be used to <strong>push</strong> to this repository? Deploy keys always have pull access.
9981002
settings.no_deploy_keys = You haven't added any deploy keys.
9991003
settings.title = Title
10001004
settings.deploy_key_content = Content

routers/api/v1/repo/key.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ func CreateDeployKey(ctx *context.APIContext, form api.CreateKeyOption) {
160160
return
161161
}
162162

163-
key, err := models.AddDeployKey(ctx.Repo.Repository.ID, form.Title, content)
163+
key, err := models.AddDeployKey(ctx.Repo.Repository.ID, form.Title, content, form.ReadOnly)
164164
if err != nil {
165165
HandleAddKeyError(ctx, err)
166166
return

routers/repo/setting.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -544,7 +544,7 @@ func DeployKeysPost(ctx *context.Context, form auth.AddKeyForm) {
544544
return
545545
}
546546

547-
key, err := models.AddDeployKey(ctx.Repo.Repository.ID, form.Title, content)
547+
key, err := models.AddDeployKey(ctx.Repo.Repository.ID, form.Title, content, !form.IsWritable)
548548
if err != nil {
549549
ctx.Data["HasError"] = true
550550
switch {

routers/repo/settings_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright 2017 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package repo
6+
7+
import (
8+
"net/http"
9+
"testing"
10+
11+
"code.gitea.io/gitea/models"
12+
"code.gitea.io/gitea/modules/auth"
13+
"code.gitea.io/gitea/modules/test"
14+
15+
"github.com/stretchr/testify/assert"
16+
)
17+
18+
func TestAddReadOnlyDeployKey(t *testing.T) {
19+
models.PrepareTestEnv(t)
20+
21+
ctx := test.MockContext(t, "user2/repo1/settings/keys")
22+
23+
test.LoadUser(t, ctx, 2)
24+
test.LoadRepo(t, ctx, 2)
25+
26+
addKeyForm := auth.AddKeyForm{
27+
Title: "read-only",
28+
Content: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDAu7tvIvX6ZHrRXuZNfkR3XLHSsuCK9Zn3X58lxBcQzuo5xZgB6vRwwm/QtJuF+zZPtY5hsQILBLmF+BZ5WpKZp1jBeSjH2G7lxet9kbcH+kIVj0tPFEoyKI9wvWqIwC4prx/WVk2wLTJjzBAhyNxfEq7C9CeiX9pQEbEqJfkKCQ== nocomment\n",
29+
}
30+
DeployKeysPost(ctx, addKeyForm)
31+
assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
32+
33+
models.AssertExistsAndLoadBean(t, &models.DeployKey{
34+
Name: addKeyForm.Title,
35+
Content: addKeyForm.Content,
36+
Mode: models.AccessModeRead,
37+
})
38+
}
39+
40+
func TestAddReadWriteOnlyDeployKey(t *testing.T) {
41+
models.PrepareTestEnv(t)
42+
43+
ctx := test.MockContext(t, "user2/repo1/settings/keys")
44+
45+
test.LoadUser(t, ctx, 2)
46+
test.LoadRepo(t, ctx, 2)
47+
48+
addKeyForm := auth.AddKeyForm{
49+
Title: "read-write",
50+
Content: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDAu7tvIvX6ZHrRXuZNfkR3XLHSsuCK9Zn3X58lxBcQzuo5xZgB6vRwwm/QtJuF+zZPtY5hsQILBLmF+BZ5WpKZp1jBeSjH2G7lxet9kbcH+kIVj0tPFEoyKI9wvWqIwC4prx/WVk2wLTJjzBAhyNxfEq7C9CeiX9pQEbEqJfkKCQ== nocomment\n",
51+
IsWritable: true,
52+
}
53+
DeployKeysPost(ctx, addKeyForm)
54+
assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
55+
56+
models.AssertExistsAndLoadBean(t, &models.DeployKey{
57+
Name: addKeyForm.Title,
58+
Content: addKeyForm.Content,
59+
Mode: models.AccessModeWrite,
60+
})
61+
}

templates/repo/settings/deploy_keys.tmpl

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
{{.Fingerprint}}
3232
</div>
3333
<div class="activity meta">
34-
<i>{{$.i18n.Tr "settings.add_on"}} <span>{{.CreatedUnix.FormatShort}}</span> — <i class="octicon octicon-info"></i> {{if .HasUsed}}{{$.i18n.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="green"{{end}}>{{.UpdatedUnix.FormatShort}}</span>{{else}}{{$.i18n.Tr "settings.no_activity"}}{{end}}</i>
34+
<i>{{$.i18n.Tr "settings.add_on"}} <span>{{.CreatedUnix.FormatShort}}</span> — <i class="octicon octicon-info"></i> {{if .HasUsed}}{{$.i18n.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="green"{{end}}>{{.UpdatedUnix.FormatShort}}</span>{{else}}{{$.i18n.Tr "settings.no_activity"}}{{end}} - <span>{{$.i18n.Tr "settings.can_read_info"}}{{if not .IsReadOnly}} / {{$.i18n.Tr "settings.can_write_info"}} {{end}}</i>
3535
</div>
3636
</div>
3737
</div>
@@ -60,6 +60,15 @@
6060
<label for="content">{{.i18n.Tr "repo.settings.deploy_key_content"}}</label>
6161
<textarea id="ssh-key-content" name="content" required>{{.content}}</textarea>
6262
</div>
63+
<div class="field">
64+
<div class="ui checkbox {{if .Err_IsWritable}}error{{end}}">
65+
<input id="ssh-key-is-writable" name="is_writable" class="hidden" type="checkbox" value="1">
66+
<label for="is_writable">
67+
{{.i18n.Tr "repo.settings.is_writable"}}
68+
</label>
69+
<small style="padding-left: 26px;">{{$.i18n.Tr "repo.settings.is_writable_info" | Str2html}}</small>
70+
</div>
71+
</div>
6372
<button class="ui green button">
6473
{{.i18n.Tr "repo.settings.add_deploy_key"}}
6574
</button>

vendor/code.gitea.io/sdk/gitea/repo_key.go

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vendor/vendor.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99
"revisionTime": "2017-12-22T02:43:26Z"
1010
},
1111
{
12-
"checksumSHA1": "QQ7g7B9+EIzGjO14KCGEs9TNEzM=",
12+
"checksumSHA1": "Qtq0kW+BnpYMOriaoCjMa86WGG8=",
1313
"path": "code.gitea.io/sdk/gitea",
14-
"revision": "ec7d3af43b598c1a3f2cb12f633b9625649d8e54",
15-
"revisionTime": "2017-11-28T12:30:39Z"
14+
"revision": "79eee8f12c7fc1cc5b802c5cdc5b494ef3733866",
15+
"revisionTime": "2017-12-20T06:57:50Z"
1616
},
1717
{
1818
"checksumSHA1": "bOODD4Gbw3GfcuQPU2dI40crxxk=",

0 commit comments

Comments
 (0)