Keys API changes (#4960)

* Add private information to the deploy keys api

This commit adds more information to the deploy keys to allow for back
reference in to the main keys list. It also adds information about the
repository that the key is referring to.

Signed-off-by: Andrew Thornton <art27@cantab.net>

* Add private information to the user keys API

This adjusts the keys API to give out private information to user keys if
the current user is the owner or an admin.

Signed-off-by: Andrew Thornton <art27@cantab.net>

* Add ability to search keys by fingerprint

This commit adds the functionality to search ssh-keys by fingerprint of
the ssh-key. Deploy keys per repository can also be searched. There is
no current clear API point to allow search of all deploy keys by
fingerprint or keyID.

Signed-off-by: Andrew Thornton <art27@cantab.net>

* Add integration test
This commit is contained in:
zeripath 2018-11-01 03:40:49 +00:00 committed by techknowlogick
parent 584844eada
commit 00533d3870
6 changed files with 276 additions and 14 deletions

View file

@ -7,8 +7,11 @@ package integrations
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"testing" "testing"
"github.com/stretchr/testify/assert"
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
api "code.gitea.io/sdk/gitea" api "code.gitea.io/sdk/gitea"
) )
@ -90,3 +93,102 @@ func TestCreateReadWriteDeployKey(t *testing.T) {
Mode: models.AccessModeWrite, Mode: models.AccessModeWrite,
}) })
} }
func TestCreateUserKey(t *testing.T) {
prepareTestEnv(t)
user := models.AssertExistsAndLoadBean(t, &models.User{Name: "user1"}).(*models.User)
session := loginUser(t, "user1")
token := url.QueryEscape(getTokenForLoggedInUser(t, session))
keysURL := fmt.Sprintf("/api/v1/user/keys?token=%s", token)
keyType := "ssh-rsa"
keyContent := "AAAAB3NzaC1yc2EAAAADAQABAAABAQCyTiPTeHJl6Gs5D1FyHT0qTWpVkAy9+LIKjctQXklrePTvUNVrSpt4r2exFYXNMPeA8V0zCrc3Kzs1SZw3jWkG3i53te9onCp85DqyatxOD2pyZ30/gPn1ZUg40WowlFM8gsUFMZqaH7ax6d8nsBKW7N/cRyqesiOQEV9up3tnKjIB8XMTVvC5X4rBWgywz7AFxSv8mmaTHnUgVW4LgMPwnTWo0pxtiIWbeMLyrEE4hIM74gSwp6CRQYo6xnG3fn4yWkcK2X2mT9adQ241IDdwpENJHcry/T6AJ8dNXduEZ67egnk+rVlQ2HM4LpymAv9DAAFFeaQK0hT+3aMDoumV"
rawKeyBody := api.CreateKeyOption{
Title: "test-key",
Key: keyType + " " + keyContent,
}
req := NewRequestWithJSON(t, "POST", keysURL, rawKeyBody)
resp := session.MakeRequest(t, req, http.StatusCreated)
var newPublicKey api.PublicKey
DecodeJSON(t, resp, &newPublicKey)
models.AssertExistsAndLoadBean(t, &models.PublicKey{
ID: newPublicKey.ID,
OwnerID: user.ID,
Name: rawKeyBody.Title,
Content: rawKeyBody.Key,
Mode: models.AccessModeWrite,
})
// Search by fingerprint
fingerprintURL := fmt.Sprintf("/api/v1/user/keys?token=%s&fingerprint=%s", token, newPublicKey.Fingerprint)
req = NewRequest(t, "GET", fingerprintURL)
resp = session.MakeRequest(t, req, http.StatusOK)
var fingerprintPublicKeys []api.PublicKey
DecodeJSON(t, resp, &fingerprintPublicKeys)
assert.Equal(t, newPublicKey.Fingerprint, fingerprintPublicKeys[0].Fingerprint)
assert.Equal(t, newPublicKey.ID, fingerprintPublicKeys[0].ID)
assert.Equal(t, user.ID, fingerprintPublicKeys[0].Owner.ID)
fingerprintURL = fmt.Sprintf("/api/v1/users/%s/keys?token=%s&fingerprint=%s", user.Name, token, newPublicKey.Fingerprint)
req = NewRequest(t, "GET", fingerprintURL)
resp = session.MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &fingerprintPublicKeys)
assert.Equal(t, newPublicKey.Fingerprint, fingerprintPublicKeys[0].Fingerprint)
assert.Equal(t, newPublicKey.ID, fingerprintPublicKeys[0].ID)
assert.Equal(t, user.ID, fingerprintPublicKeys[0].Owner.ID)
// Fail search by fingerprint
fingerprintURL = fmt.Sprintf("/api/v1/user/keys?token=%s&fingerprint=%sA", token, newPublicKey.Fingerprint)
req = NewRequest(t, "GET", fingerprintURL)
resp = session.MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &fingerprintPublicKeys)
assert.Len(t, fingerprintPublicKeys, 0)
// Fail searching for wrong users key
fingerprintURL = fmt.Sprintf("/api/v1/users/%s/keys?token=%s&fingerprint=%s", "user2", token, newPublicKey.Fingerprint)
req = NewRequest(t, "GET", fingerprintURL)
resp = session.MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &fingerprintPublicKeys)
assert.Len(t, fingerprintPublicKeys, 0)
// Now login as user 2
session2 := loginUser(t, "user2")
token2 := url.QueryEscape(getTokenForLoggedInUser(t, session2))
// Should find key even though not ours, but we shouldn't know whose it is
fingerprintURL = fmt.Sprintf("/api/v1/user/keys?token=%s&fingerprint=%s", token2, newPublicKey.Fingerprint)
req = NewRequest(t, "GET", fingerprintURL)
resp = session.MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &fingerprintPublicKeys)
assert.Equal(t, newPublicKey.Fingerprint, fingerprintPublicKeys[0].Fingerprint)
assert.Equal(t, newPublicKey.ID, fingerprintPublicKeys[0].ID)
assert.Nil(t, fingerprintPublicKeys[0].Owner)
// Should find key even though not ours, but we shouldn't know whose it is
fingerprintURL = fmt.Sprintf("/api/v1/users/%s/keys?token=%s&fingerprint=%s", user.Name, token2, newPublicKey.Fingerprint)
req = NewRequest(t, "GET", fingerprintURL)
resp = session.MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &fingerprintPublicKeys)
assert.Equal(t, newPublicKey.Fingerprint, fingerprintPublicKeys[0].Fingerprint)
assert.Equal(t, newPublicKey.ID, fingerprintPublicKeys[0].ID)
assert.Nil(t, fingerprintPublicKeys[0].Owner)
// Fail when searching for key if it is not ours
fingerprintURL = fmt.Sprintf("/api/v1/users/%s/keys?token=%s&fingerprint=%s", "user2", token2, newPublicKey.Fingerprint)
req = NewRequest(t, "GET", fingerprintURL)
resp = session.MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &fingerprintPublicKeys)
assert.Len(t, fingerprintPublicKeys, 0)
}

View file

@ -24,6 +24,7 @@ import (
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"github.com/Unknwon/com" "github.com/Unknwon/com"
"github.com/go-xorm/builder"
"github.com/go-xorm/xorm" "github.com/go-xorm/xorm"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
@ -465,6 +466,19 @@ func SearchPublicKeyByContent(content string) (*PublicKey, error) {
return key, nil return key, nil
} }
// SearchPublicKey returns a list of public keys matching the provided arguments.
func SearchPublicKey(uid int64, fingerprint string) ([]*PublicKey, error) {
keys := make([]*PublicKey, 0, 5)
cond := builder.NewCond()
if uid != 0 {
cond = cond.And(builder.Eq{"owner_id": uid})
}
if fingerprint != "" {
cond = cond.And(builder.Eq{"fingerprint": fingerprint})
}
return keys, x.Where(cond).Find(&keys)
}
// ListPublicKeys returns a list of public keys belongs to given user. // ListPublicKeys returns a list of public keys belongs to given user.
func ListPublicKeys(uid int64) ([]*PublicKey, error) { func ListPublicKeys(uid int64) ([]*PublicKey, error) {
keys := make([]*PublicKey, 0, 5) keys := make([]*PublicKey, 0, 5)
@ -833,3 +847,19 @@ func ListDeployKeys(repoID int64) ([]*DeployKey, error) {
Where("repo_id = ?", repoID). Where("repo_id = ?", repoID).
Find(&keys) Find(&keys)
} }
// SearchDeployKeys returns a list of deploy keys matching the provided arguments.
func SearchDeployKeys(repoID int64, keyID int64, fingerprint string) ([]*DeployKey, error) {
keys := make([]*DeployKey, 0, 5)
cond := builder.NewCond()
if repoID != 0 {
cond = cond.And(builder.Eq{"repo_id": repoID})
}
if keyID != 0 {
cond = cond.And(builder.Eq{"key_id": keyID})
}
if fingerprint != "" {
cond = cond.And(builder.Eq{"fingerprint": fingerprint})
}
return keys, x.Where(cond).Find(&keys)
}

View file

@ -167,12 +167,14 @@ func ToHook(repoLink string, w *models.Webhook) *api.Hook {
// ToDeployKey convert models.DeployKey to api.DeployKey // ToDeployKey convert models.DeployKey to api.DeployKey
func ToDeployKey(apiLink string, key *models.DeployKey) *api.DeployKey { func ToDeployKey(apiLink string, key *models.DeployKey) *api.DeployKey {
return &api.DeployKey{ return &api.DeployKey{
ID: key.ID, ID: key.ID,
Key: key.Content, KeyID: key.KeyID,
URL: apiLink + com.ToStr(key.ID), Key: key.Content,
Title: key.Name, Fingerprint: key.Fingerprint,
Created: key.CreatedUnix.AsTime(), URL: apiLink + com.ToStr(key.ID),
ReadOnly: true, // All deploy keys are read-only. Title: key.Name,
Created: key.CreatedUnix.AsTime(),
ReadOnly: key.Mode == models.AccessModeRead, // All deploy keys are read-only.
} }
} }

View file

@ -15,6 +15,21 @@ import (
api "code.gitea.io/sdk/gitea" api "code.gitea.io/sdk/gitea"
) )
// appendPrivateInformation appends the owner and key type information to api.PublicKey
func appendPrivateInformation(apiKey *api.DeployKey, key *models.DeployKey, repository *models.Repository) (*api.DeployKey, error) {
apiKey.ReadOnly = key.Mode == models.AccessModeRead
if repository.ID == key.RepoID {
apiKey.Repository = repository.APIFormat(key.Mode)
} else {
repo, err := models.GetRepositoryByID(key.RepoID)
if err != nil {
return apiKey, err
}
apiKey.Repository = repo.APIFormat(key.Mode)
}
return apiKey, nil
}
func composeDeployKeysAPILink(repoPath string) string { func composeDeployKeysAPILink(repoPath string) string {
return setting.AppURL + "api/v1/repos/" + repoPath + "/keys/" return setting.AppURL + "api/v1/repos/" + repoPath + "/keys/"
} }
@ -37,10 +52,28 @@ func ListDeployKeys(ctx *context.APIContext) {
// description: name of the repo // description: name of the repo
// type: string // type: string
// required: true // required: true
// - name: key_id
// in: query
// description: the key_id to search for
// type: integer
// - name: fingerprint
// in: query
// description: fingerprint of the key
// type: string
// responses: // responses:
// "200": // "200":
// "$ref": "#/responses/DeployKeyList" // "$ref": "#/responses/DeployKeyList"
keys, err := models.ListDeployKeys(ctx.Repo.Repository.ID) var keys []*models.DeployKey
var err error
fingerprint := ctx.Query("fingerprint")
keyID := ctx.QueryInt64("key_id")
if fingerprint != "" || keyID != 0 {
keys, err = models.SearchDeployKeys(ctx.Repo.Repository.ID, keyID, fingerprint)
} else {
keys, err = models.ListDeployKeys(ctx.Repo.Repository.ID)
}
if err != nil { if err != nil {
ctx.Error(500, "ListDeployKeys", err) ctx.Error(500, "ListDeployKeys", err)
return return
@ -54,6 +87,9 @@ func ListDeployKeys(ctx *context.APIContext) {
return return
} }
apiKeys[i] = convert.ToDeployKey(apiLink, keys[i]) apiKeys[i] = convert.ToDeployKey(apiLink, keys[i])
if ctx.User.IsAdmin || ((ctx.Repo.Repository.ID == keys[i].RepoID) && (ctx.User.ID == ctx.Repo.Owner.ID)) {
apiKeys[i], _ = appendPrivateInformation(apiKeys[i], keys[i], ctx.Repo.Repository)
}
} }
ctx.JSON(200, &apiKeys) ctx.JSON(200, &apiKeys)
@ -102,7 +138,11 @@ func GetDeployKey(ctx *context.APIContext) {
} }
apiLink := composeDeployKeysAPILink(ctx.Repo.Owner.Name + "/" + ctx.Repo.Repository.Name) apiLink := composeDeployKeysAPILink(ctx.Repo.Owner.Name + "/" + ctx.Repo.Repository.Name)
ctx.JSON(200, convert.ToDeployKey(apiLink, key)) apiKey := convert.ToDeployKey(apiLink, key)
if ctx.User.IsAdmin || ((ctx.Repo.Repository.ID == key.RepoID) && (ctx.User.ID == ctx.Repo.Owner.ID)) {
apiKey, _ = appendPrivateInformation(apiKey, key, ctx.Repo.Repository)
}
ctx.JSON(200, apiKey)
} }
// HandleCheckKeyStringError handle check key error // HandleCheckKeyStringError handle check key error

View file

@ -14,6 +14,29 @@ import (
"code.gitea.io/gitea/routers/api/v1/repo" "code.gitea.io/gitea/routers/api/v1/repo"
) )
// appendPrivateInformation appends the owner and key type information to api.PublicKey
func appendPrivateInformation(apiKey *api.PublicKey, key *models.PublicKey, defaultUser *models.User) (*api.PublicKey, error) {
if key.Type == models.KeyTypeDeploy {
apiKey.KeyType = "deploy"
} else if key.Type == models.KeyTypeUser {
apiKey.KeyType = "user"
if defaultUser.ID == key.OwnerID {
apiKey.Owner = defaultUser.APIFormat()
} else {
user, err := models.GetUserByID(key.OwnerID)
if err != nil {
return apiKey, err
}
apiKey.Owner = user.APIFormat()
}
} else {
apiKey.KeyType = "unknown"
}
apiKey.ReadOnly = key.Mode == models.AccessModeRead
return apiKey, nil
}
// GetUserByParamsName get user by name // GetUserByParamsName get user by name
func GetUserByParamsName(ctx *context.APIContext, name string) *models.User { func GetUserByParamsName(ctx *context.APIContext, name string) *models.User {
user, err := models.GetUserByName(ctx.Params(name)) user, err := models.GetUserByName(ctx.Params(name))
@ -37,8 +60,27 @@ func composePublicKeysAPILink() string {
return setting.AppURL + "api/v1/user/keys/" return setting.AppURL + "api/v1/user/keys/"
} }
func listPublicKeys(ctx *context.APIContext, uid int64) { func listPublicKeys(ctx *context.APIContext, user *models.User) {
keys, err := models.ListPublicKeys(uid) var keys []*models.PublicKey
var err error
fingerprint := ctx.Query("fingerprint")
username := ctx.Params("username")
if fingerprint != "" {
// Querying not just listing
if username != "" {
// Restrict to provided uid
keys, err = models.SearchPublicKey(user.ID, fingerprint)
} else {
// Unrestricted
keys, err = models.SearchPublicKey(0, fingerprint)
}
} else {
// Use ListPublicKeys
keys, err = models.ListPublicKeys(user.ID)
}
if err != nil { if err != nil {
ctx.Error(500, "ListPublicKeys", err) ctx.Error(500, "ListPublicKeys", err)
return return
@ -48,6 +90,9 @@ func listPublicKeys(ctx *context.APIContext, uid int64) {
apiKeys := make([]*api.PublicKey, len(keys)) apiKeys := make([]*api.PublicKey, len(keys))
for i := range keys { for i := range keys {
apiKeys[i] = convert.ToPublicKey(apiLink, keys[i]) apiKeys[i] = convert.ToPublicKey(apiLink, keys[i])
if ctx.User.IsAdmin || ctx.User.ID == keys[i].OwnerID {
apiKeys[i], _ = appendPrivateInformation(apiKeys[i], keys[i], user)
}
} }
ctx.JSON(200, &apiKeys) ctx.JSON(200, &apiKeys)
@ -58,12 +103,17 @@ func ListMyPublicKeys(ctx *context.APIContext) {
// swagger:operation GET /user/keys user userCurrentListKeys // swagger:operation GET /user/keys user userCurrentListKeys
// --- // ---
// summary: List the authenticated user's public keys // summary: List the authenticated user's public keys
// parameters:
// - name: fingerprint
// in: query
// description: fingerprint of the key
// type: string
// produces: // produces:
// - application/json // - application/json
// responses: // responses:
// "200": // "200":
// "$ref": "#/responses/PublicKeyList" // "$ref": "#/responses/PublicKeyList"
listPublicKeys(ctx, ctx.User.ID) listPublicKeys(ctx, ctx.User)
} }
// ListPublicKeys list the given user's public keys // ListPublicKeys list the given user's public keys
@ -79,6 +129,10 @@ func ListPublicKeys(ctx *context.APIContext) {
// description: username of user // description: username of user
// type: string // type: string
// required: true // required: true
// - name: fingerprint
// in: query
// description: fingerprint of the key
// type: string
// responses: // responses:
// "200": // "200":
// "$ref": "#/responses/PublicKeyList" // "$ref": "#/responses/PublicKeyList"
@ -86,7 +140,7 @@ func ListPublicKeys(ctx *context.APIContext) {
if ctx.Written() { if ctx.Written() {
return return
} }
listPublicKeys(ctx, user.ID) listPublicKeys(ctx, user)
} }
// GetPublicKey get a public key // GetPublicKey get a public key
@ -119,7 +173,11 @@ func GetPublicKey(ctx *context.APIContext) {
} }
apiLink := composePublicKeysAPILink() apiLink := composePublicKeysAPILink()
ctx.JSON(200, convert.ToPublicKey(apiLink, key)) apiKey := convert.ToPublicKey(apiLink, key)
if ctx.User.IsAdmin || ctx.User.ID == key.OwnerID {
apiKey, _ = appendPrivateInformation(apiKey, key, ctx.User)
}
ctx.JSON(200, apiKey)
} }
// CreateUserPublicKey creates new public key to given user by ID. // CreateUserPublicKey creates new public key to given user by ID.
@ -136,7 +194,11 @@ func CreateUserPublicKey(ctx *context.APIContext, form api.CreateKeyOption, uid
return return
} }
apiLink := composePublicKeysAPILink() apiLink := composePublicKeysAPILink()
ctx.JSON(201, convert.ToPublicKey(apiLink, key)) apiKey := convert.ToPublicKey(apiLink, key)
if ctx.User.IsAdmin || ctx.User.ID == key.OwnerID {
apiKey, _ = appendPrivateInformation(apiKey, key, ctx.User)
}
ctx.JSON(201, apiKey)
} }
// CreatePublicKey create one public key for me // CreatePublicKey create one public key for me

View file

@ -2682,6 +2682,18 @@
"name": "repo", "name": "repo",
"in": "path", "in": "path",
"required": true "required": true
},
{
"type": "integer",
"description": "the key_id to search for",
"name": "key_id",
"in": "query"
},
{
"type": "string",
"description": "fingerprint of the key",
"name": "fingerprint",
"in": "query"
} }
], ],
"responses": { "responses": {
@ -4976,6 +4988,14 @@
], ],
"summary": "List the authenticated user's public keys", "summary": "List the authenticated user's public keys",
"operationId": "userCurrentListKeys", "operationId": "userCurrentListKeys",
"parameters": [
{
"type": "string",
"description": "fingerprint of the key",
"name": "fingerprint",
"in": "query"
}
],
"responses": { "responses": {
"200": { "200": {
"$ref": "#/responses/PublicKeyList" "$ref": "#/responses/PublicKeyList"
@ -5540,6 +5560,12 @@
"name": "username", "name": "username",
"in": "path", "in": "path",
"required": true "required": true
},
{
"type": "string",
"description": "fingerprint of the key",
"name": "fingerprint",
"in": "query"
} }
], ],
"responses": { "responses": {