* 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 testtags/v1.7.0-dev
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" | ||||
) | ) | ||||
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) | |||||
} |
"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" | ||||
) | ) | ||||
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) | ||||
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) | |||||
} |
// 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, | |||||
Key: key.Content, | |||||
URL: apiLink + com.ToStr(key.ID), | |||||
Title: key.Name, | |||||
Created: key.CreatedUnix.AsTime(), | |||||
ReadOnly: true, // All deploy keys are read-only. | |||||
ID: key.ID, | |||||
KeyID: key.KeyID, | |||||
Key: key.Content, | |||||
Fingerprint: key.Fingerprint, | |||||
URL: apiLink + com.ToStr(key.ID), | |||||
Title: key.Name, | |||||
Created: key.CreatedUnix.AsTime(), | |||||
ReadOnly: key.Mode == models.AccessModeRead, // All deploy keys are read-only. | |||||
} | } | ||||
} | } | ||||
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/" | ||||
} | } | ||||
// 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 | ||||
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) | ||||
} | } | ||||
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 |
"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)) | ||||
return setting.AppURL + "api/v1/user/keys/" | return setting.AppURL + "api/v1/user/keys/" | ||||
} | } | ||||
func listPublicKeys(ctx *context.APIContext, uid int64) { | |||||
keys, err := models.ListPublicKeys(uid) | |||||
func listPublicKeys(ctx *context.APIContext, user *models.User) { | |||||
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 | ||||
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) | ||||
// 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 | ||||
// 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" | ||||
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 | ||||
} | } | ||||
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. | ||||
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 |
"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": { | ||||
], | ], | ||||
"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" | ||||
"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": { |