* 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 helltags/v1.4.0-rc1
package integrations | package integrations | ||||
import ( | import ( | ||||
"fmt" | |||||
"net/http" | "net/http" | ||||
"testing" | "testing" | ||||
"code.gitea.io/gitea/models" | |||||
api "code.gitea.io/sdk/gitea" | api "code.gitea.io/sdk/gitea" | ||||
) | ) | ||||
req := NewRequest(t, "DELETE", "/api/v1/repos/user2/repo1/keys/1") | req := NewRequest(t, "DELETE", "/api/v1/repos/user2/repo1/keys/1") | ||||
MakeRequest(t, req, http.StatusUnauthorized) | MakeRequest(t, req, http.StatusUnauthorized) | ||||
} | } | ||||
func TestCreateReadOnlyDeployKey(t *testing.T) { | |||||
prepareTestEnv(t) | |||||
repo := models.AssertExistsAndLoadBean(t, &models.Repository{Name: "repo1"}).(*models.Repository) | |||||
repoOwner := models.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User) | |||||
session := loginUser(t, repoOwner.Name) | |||||
keysURL := fmt.Sprintf("/api/v1/repos/%s/%s/keys", repoOwner.Name, repo.Name) | |||||
rawKeyBody := api.CreateKeyOption{ | |||||
Title: "read-only", | |||||
Key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDAu7tvIvX6ZHrRXuZNfkR3XLHSsuCK9Zn3X58lxBcQzuo5xZgB6vRwwm/QtJuF+zZPtY5hsQILBLmF+BZ5WpKZp1jBeSjH2G7lxet9kbcH+kIVj0tPFEoyKI9wvWqIwC4prx/WVk2wLTJjzBAhyNxfEq7C9CeiX9pQEbEqJfkKCQ== nocomment\n", | |||||
ReadOnly: true, | |||||
} | |||||
req := NewRequestWithJSON(t, "POST", keysURL, rawKeyBody) | |||||
resp := session.MakeRequest(t, req, http.StatusCreated) | |||||
var newDeployKey api.DeployKey | |||||
DecodeJSON(t, resp, &newDeployKey) | |||||
models.AssertExistsAndLoadBean(t, &models.DeployKey{ | |||||
ID: newDeployKey.ID, | |||||
Name: rawKeyBody.Title, | |||||
Content: rawKeyBody.Key, | |||||
Mode: models.AccessModeRead, | |||||
}) | |||||
} | |||||
func TestCreateReadWriteDeployKey(t *testing.T) { | |||||
prepareTestEnv(t) | |||||
repo := models.AssertExistsAndLoadBean(t, &models.Repository{Name: "repo1"}).(*models.Repository) | |||||
repoOwner := models.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User) | |||||
session := loginUser(t, repoOwner.Name) | |||||
keysURL := fmt.Sprintf("/api/v1/repos/%s/%s/keys", repoOwner.Name, repo.Name) | |||||
rawKeyBody := api.CreateKeyOption{ | |||||
Title: "read-write", | |||||
Key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDsufOCrDDlT8DLkodnnJtbq7uGflcPae7euTfM+Laq4So+v4WeSV362Rg0O/+Sje1UthrhN6lQkfRkdWIlCRQEXg+LMqr6RhvDfZquE2Xwqv/itlz7LjbdAUdYoO1iH7rMSmYvQh4WEnC/DAacKGbhdGIM/ZBz0z6tHm7bPgbI9ykEKekTmPwQFP1Qebvf5NYOFMWqQ2sCEAI9dBMVLoojsIpV+KADf+BotiIi8yNfTG2rzmzpxBpW9fYjd1Sy1yd4NSUpoPbEJJYJ1TrjiSWlYOVq9Ar8xW1O87i6gBjL/3zN7ANeoYhaAXupdOS6YL22YOK/yC0tJtXwwdh/eSrh", | |||||
} | |||||
req := NewRequestWithJSON(t, "POST", keysURL, rawKeyBody) | |||||
resp := session.MakeRequest(t, req, http.StatusCreated) | |||||
var newDeployKey api.DeployKey | |||||
DecodeJSON(t, resp, &newDeployKey) | |||||
models.AssertExistsAndLoadBean(t, &models.DeployKey{ | |||||
ID: newDeployKey.ID, | |||||
Name: rawKeyBody.Title, | |||||
Content: rawKeyBody.Key, | |||||
Mode: models.AccessModeWrite, | |||||
}) | |||||
} |
[] # empty |
NewMigration("add reactions", addReactions), | NewMigration("add reactions", addReactions), | ||||
// v54 -> v55 | // v54 -> v55 | ||||
NewMigration("add pull request options", addPullRequestOptions), | NewMigration("add pull request options", addPullRequestOptions), | ||||
// v55 -> v56 | |||||
NewMigration("add writable deploy keys", addModeToDeploKeys), | |||||
} | } | ||||
// Migrate database to current version | // Migrate database to current version |
// Copyright 2018 The Gitea Authors. All rights reserved. | |||||
// Use of this source code is governed by a MIT-style | |||||
// license that can be found in the LICENSE file. | |||||
package migrations | |||||
import ( | |||||
"fmt" | |||||
"code.gitea.io/gitea/models" | |||||
"github.com/go-xorm/xorm" | |||||
) | |||||
func addModeToDeploKeys(x *xorm.Engine) error { | |||||
type DeployKey struct { | |||||
Mode models.AccessMode `xorm:"NOT NULL DEFAULT 1"` | |||||
} | |||||
if err := x.Sync2(new(DeployKey)); err != nil { | |||||
return fmt.Errorf("Sync2: %v", err) | |||||
} | |||||
return nil | |||||
} |
Fingerprint string | Fingerprint string | ||||
Content string `xorm:"-"` | Content string `xorm:"-"` | ||||
Mode AccessMode `xorm:"NOT NULL DEFAULT 1"` | |||||
CreatedUnix util.TimeStamp `xorm:"created"` | CreatedUnix util.TimeStamp `xorm:"created"` | ||||
UpdatedUnix util.TimeStamp `xorm:"updated"` | UpdatedUnix util.TimeStamp `xorm:"updated"` | ||||
HasRecentActivity bool `xorm:"-"` | HasRecentActivity bool `xorm:"-"` | ||||
return nil | return nil | ||||
} | } | ||||
// IsReadOnly checks if the key can only be used for read operations | |||||
func (key *DeployKey) IsReadOnly() bool { | |||||
return key.Mode == AccessModeRead | |||||
} | |||||
func checkDeployKey(e Engine, keyID, repoID int64, name string) error { | func checkDeployKey(e Engine, keyID, repoID int64, name string) error { | ||||
// Note: We want error detail, not just true or false here. | // Note: We want error detail, not just true or false here. | ||||
has, err := e. | has, err := e. | ||||
} | } | ||||
// addDeployKey adds new key-repo relation. | // addDeployKey adds new key-repo relation. | ||||
func addDeployKey(e *xorm.Session, keyID, repoID int64, name, fingerprint string) (*DeployKey, error) { | |||||
func addDeployKey(e *xorm.Session, keyID, repoID int64, name, fingerprint string, mode AccessMode) (*DeployKey, error) { | |||||
if err := checkDeployKey(e, keyID, repoID, name); err != nil { | if err := checkDeployKey(e, keyID, repoID, name); err != nil { | ||||
return nil, err | return nil, err | ||||
} | } | ||||
RepoID: repoID, | RepoID: repoID, | ||||
Name: name, | Name: name, | ||||
Fingerprint: fingerprint, | Fingerprint: fingerprint, | ||||
Mode: mode, | |||||
} | } | ||||
_, err := e.Insert(key) | _, err := e.Insert(key) | ||||
return key, err | return key, err | ||||
} | } | ||||
// AddDeployKey add new deploy key to database and authorized_keys file. | // AddDeployKey add new deploy key to database and authorized_keys file. | ||||
func AddDeployKey(repoID int64, name, content string) (*DeployKey, error) { | |||||
func AddDeployKey(repoID int64, name, content string, readOnly bool) (*DeployKey, error) { | |||||
fingerprint, err := calcFingerprint(content) | fingerprint, err := calcFingerprint(content) | ||||
if err != nil { | if err != nil { | ||||
return nil, err | return nil, err | ||||
} | } | ||||
accessMode := AccessModeRead | |||||
if !readOnly { | |||||
accessMode = AccessModeWrite | |||||
} | |||||
pkey := &PublicKey{ | pkey := &PublicKey{ | ||||
Fingerprint: fingerprint, | Fingerprint: fingerprint, | ||||
Mode: AccessModeRead, | |||||
Mode: accessMode, | |||||
Type: KeyTypeDeploy, | Type: KeyTypeDeploy, | ||||
} | } | ||||
has, err := x.Get(pkey) | has, err := x.Get(pkey) | ||||
} | } | ||||
} | } | ||||
key, err := addDeployKey(sess, pkey.ID, repoID, name, pkey.Fingerprint) | |||||
key, err := addDeployKey(sess, pkey.ID, repoID, name, pkey.Fingerprint, accessMode) | |||||
if err != nil { | if err != nil { | ||||
return nil, fmt.Errorf("addDeployKey: %v", err) | return nil, fmt.Errorf("addDeployKey: %v", err) | ||||
} | } |
// AddKeyForm form for adding SSH/GPG key | // AddKeyForm form for adding SSH/GPG key | ||||
type AddKeyForm struct { | type AddKeyForm struct { | ||||
Type string `binding:"OmitEmpty"` | |||||
Title string `binding:"Required;MaxSize(50)"` | |||||
Content string `binding:"Required"` | |||||
Type string `binding:"OmitEmpty"` | |||||
Title string `binding:"Required;MaxSize(50)"` | |||||
Content string `binding:"Required"` | |||||
IsWritable bool | |||||
} | } | ||||
// Validate validates the fields | // Validate validates the fields |
valid_forever = Valid forever | valid_forever = Valid forever | ||||
last_used = Last used on | last_used = Last used on | ||||
no_activity = No recent activity | no_activity = No recent activity | ||||
can_read_info = Read | |||||
can_write_info = Write | |||||
key_state_desc = This key has been used in the last 7 days | key_state_desc = This key has been used in the last 7 days | ||||
token_state_desc = This token has been used in the last 7 days | token_state_desc = This token has been used in the last 7 days | ||||
show_openid = Show on profile | show_openid = Show on profile | ||||
settings.deploy_keys = Deploy Keys | settings.deploy_keys = Deploy Keys | ||||
settings.add_deploy_key = Add Deploy Key | settings.add_deploy_key = Add Deploy Key | ||||
settings.deploy_key_desc = Deploy keys have read-only access. They are not the same as personal account SSH keys. | settings.deploy_key_desc = Deploy keys have read-only access. They are not the same as personal account SSH keys. | ||||
settings.is_writable = Allow write access | |||||
settings.is_writable_info = Can this key be used to <strong>push</strong> to this repository? Deploy keys always have pull access. | |||||
settings.no_deploy_keys = You haven't added any deploy keys. | settings.no_deploy_keys = You haven't added any deploy keys. | ||||
settings.title = Title | settings.title = Title | ||||
settings.deploy_key_content = Content | settings.deploy_key_content = Content |
return | return | ||||
} | } | ||||
key, err := models.AddDeployKey(ctx.Repo.Repository.ID, form.Title, content) | |||||
key, err := models.AddDeployKey(ctx.Repo.Repository.ID, form.Title, content, form.ReadOnly) | |||||
if err != nil { | if err != nil { | ||||
HandleAddKeyError(ctx, err) | HandleAddKeyError(ctx, err) | ||||
return | return |
return | return | ||||
} | } | ||||
key, err := models.AddDeployKey(ctx.Repo.Repository.ID, form.Title, content) | |||||
key, err := models.AddDeployKey(ctx.Repo.Repository.ID, form.Title, content, !form.IsWritable) | |||||
if err != nil { | if err != nil { | ||||
ctx.Data["HasError"] = true | ctx.Data["HasError"] = true | ||||
switch { | switch { |
// Copyright 2017 The Gitea Authors. All rights reserved. | |||||
// Use of this source code is governed by a MIT-style | |||||
// license that can be found in the LICENSE file. | |||||
package repo | |||||
import ( | |||||
"net/http" | |||||
"testing" | |||||
"code.gitea.io/gitea/models" | |||||
"code.gitea.io/gitea/modules/auth" | |||||
"code.gitea.io/gitea/modules/test" | |||||
"github.com/stretchr/testify/assert" | |||||
) | |||||
func TestAddReadOnlyDeployKey(t *testing.T) { | |||||
models.PrepareTestEnv(t) | |||||
ctx := test.MockContext(t, "user2/repo1/settings/keys") | |||||
test.LoadUser(t, ctx, 2) | |||||
test.LoadRepo(t, ctx, 2) | |||||
addKeyForm := auth.AddKeyForm{ | |||||
Title: "read-only", | |||||
Content: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDAu7tvIvX6ZHrRXuZNfkR3XLHSsuCK9Zn3X58lxBcQzuo5xZgB6vRwwm/QtJuF+zZPtY5hsQILBLmF+BZ5WpKZp1jBeSjH2G7lxet9kbcH+kIVj0tPFEoyKI9wvWqIwC4prx/WVk2wLTJjzBAhyNxfEq7C9CeiX9pQEbEqJfkKCQ== nocomment\n", | |||||
} | |||||
DeployKeysPost(ctx, addKeyForm) | |||||
assert.EqualValues(t, http.StatusFound, ctx.Resp.Status()) | |||||
models.AssertExistsAndLoadBean(t, &models.DeployKey{ | |||||
Name: addKeyForm.Title, | |||||
Content: addKeyForm.Content, | |||||
Mode: models.AccessModeRead, | |||||
}) | |||||
} | |||||
func TestAddReadWriteOnlyDeployKey(t *testing.T) { | |||||
models.PrepareTestEnv(t) | |||||
ctx := test.MockContext(t, "user2/repo1/settings/keys") | |||||
test.LoadUser(t, ctx, 2) | |||||
test.LoadRepo(t, ctx, 2) | |||||
addKeyForm := auth.AddKeyForm{ | |||||
Title: "read-write", | |||||
Content: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDAu7tvIvX6ZHrRXuZNfkR3XLHSsuCK9Zn3X58lxBcQzuo5xZgB6vRwwm/QtJuF+zZPtY5hsQILBLmF+BZ5WpKZp1jBeSjH2G7lxet9kbcH+kIVj0tPFEoyKI9wvWqIwC4prx/WVk2wLTJjzBAhyNxfEq7C9CeiX9pQEbEqJfkKCQ== nocomment\n", | |||||
IsWritable: true, | |||||
} | |||||
DeployKeysPost(ctx, addKeyForm) | |||||
assert.EqualValues(t, http.StatusFound, ctx.Resp.Status()) | |||||
models.AssertExistsAndLoadBean(t, &models.DeployKey{ | |||||
Name: addKeyForm.Title, | |||||
Content: addKeyForm.Content, | |||||
Mode: models.AccessModeWrite, | |||||
}) | |||||
} |
{{.Fingerprint}} | {{.Fingerprint}} | ||||
</div> | </div> | ||||
<div class="activity meta"> | <div class="activity meta"> | ||||
<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> | |||||
<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> | |||||
</div> | </div> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
<label for="content">{{.i18n.Tr "repo.settings.deploy_key_content"}}</label> | <label for="content">{{.i18n.Tr "repo.settings.deploy_key_content"}}</label> | ||||
<textarea id="ssh-key-content" name="content" required>{{.content}}</textarea> | <textarea id="ssh-key-content" name="content" required>{{.content}}</textarea> | ||||
</div> | </div> | ||||
<div class="field"> | |||||
<div class="ui checkbox {{if .Err_IsWritable}}error{{end}}"> | |||||
<input id="ssh-key-is-writable" name="is_writable" class="hidden" type="checkbox" value="1"> | |||||
<label for="is_writable"> | |||||
{{.i18n.Tr "repo.settings.is_writable"}} | |||||
</label> | |||||
<small style="padding-left: 26px;">{{$.i18n.Tr "repo.settings.is_writable_info" | Str2html}}</small> | |||||
</div> | |||||
</div> | |||||
<button class="ui green button"> | <button class="ui green button"> | ||||
{{.i18n.Tr "repo.settings.add_deploy_key"}} | {{.i18n.Tr "repo.settings.add_deploy_key"}} | ||||
</button> | </button> |
// required: true | // required: true | ||||
// unique: true | // unique: true | ||||
Key string `json:"key" binding:"Required"` | Key string `json:"key" binding:"Required"` | ||||
// Describe if the key has only read access or read/write | |||||
// | |||||
// required: false | |||||
ReadOnly bool `json:"read_only"` | |||||
} | } | ||||
// CreateDeployKey options when create one deploy key | // CreateDeployKey options when create one deploy key |
"revisionTime": "2017-12-22T02:43:26Z" | "revisionTime": "2017-12-22T02:43:26Z" | ||||
}, | }, | ||||
{ | { | ||||
"checksumSHA1": "QQ7g7B9+EIzGjO14KCGEs9TNEzM=", | |||||
"checksumSHA1": "Qtq0kW+BnpYMOriaoCjMa86WGG8=", | |||||
"path": "code.gitea.io/sdk/gitea", | "path": "code.gitea.io/sdk/gitea", | ||||
"revision": "ec7d3af43b598c1a3f2cb12f633b9625649d8e54", | |||||
"revisionTime": "2017-11-28T12:30:39Z" | |||||
"revision": "79eee8f12c7fc1cc5b802c5cdc5b494ef3733866", | |||||
"revisionTime": "2017-12-20T06:57:50Z" | |||||
}, | }, | ||||
{ | { | ||||
"checksumSHA1": "bOODD4Gbw3GfcuQPU2dI40crxxk=", | "checksumSHA1": "bOODD4Gbw3GfcuQPU2dI40crxxk=", |