diff --git a/integrations/api_keys_test.go b/integrations/api_keys_test.go index 8c83ae42c50..91cbd72f917 100644 --- a/integrations/api_keys_test.go +++ b/integrations/api_keys_test.go @@ -7,8 +7,11 @@ package integrations import ( "fmt" "net/http" + "net/url" "testing" + "github.com/stretchr/testify/assert" + "code.gitea.io/gitea/models" api "code.gitea.io/sdk/gitea" ) @@ -90,3 +93,102 @@ func TestCreateReadWriteDeployKey(t *testing.T) { 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) +} diff --git a/models/ssh_key.go b/models/ssh_key.go index 2592209b4d7..0368ffad33e 100644 --- a/models/ssh_key.go +++ b/models/ssh_key.go @@ -24,6 +24,7 @@ import ( "code.gitea.io/gitea/modules/util" "github.com/Unknwon/com" + "github.com/go-xorm/builder" "github.com/go-xorm/xorm" "golang.org/x/crypto/ssh" ) @@ -465,6 +466,19 @@ func SearchPublicKeyByContent(content string) (*PublicKey, error) { 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. func ListPublicKeys(uid int64) ([]*PublicKey, error) { keys := make([]*PublicKey, 0, 5) @@ -833,3 +847,19 @@ func ListDeployKeys(repoID int64) ([]*DeployKey, error) { Where("repo_id = ?", repoID). 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) +} diff --git a/routers/api/v1/convert/convert.go b/routers/api/v1/convert/convert.go index 19b966971a4..1bfeae34bfe 100644 --- a/routers/api/v1/convert/convert.go +++ b/routers/api/v1/convert/convert.go @@ -167,12 +167,14 @@ func ToHook(repoLink string, w *models.Webhook) *api.Hook { // ToDeployKey convert models.DeployKey to api.DeployKey func ToDeployKey(apiLink string, key *models.DeployKey) *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. } } diff --git a/routers/api/v1/repo/key.go b/routers/api/v1/repo/key.go index 89a550cfd31..2caca887aad 100644 --- a/routers/api/v1/repo/key.go +++ b/routers/api/v1/repo/key.go @@ -15,6 +15,21 @@ import ( 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 { return setting.AppURL + "api/v1/repos/" + repoPath + "/keys/" } @@ -37,10 +52,28 @@ func ListDeployKeys(ctx *context.APIContext) { // description: name of the repo // type: string // 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: // "200": // "$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 { ctx.Error(500, "ListDeployKeys", err) return @@ -54,6 +87,9 @@ func ListDeployKeys(ctx *context.APIContext) { return } 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) @@ -102,7 +138,11 @@ func GetDeployKey(ctx *context.APIContext) { } 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 diff --git a/routers/api/v1/user/key.go b/routers/api/v1/user/key.go index e5d1b08f0da..d8ab752b2be 100644 --- a/routers/api/v1/user/key.go +++ b/routers/api/v1/user/key.go @@ -14,6 +14,29 @@ import ( "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 func GetUserByParamsName(ctx *context.APIContext, name string) *models.User { user, err := models.GetUserByName(ctx.Params(name)) @@ -37,8 +60,27 @@ func composePublicKeysAPILink() string { 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 { ctx.Error(500, "ListPublicKeys", err) return @@ -48,6 +90,9 @@ func listPublicKeys(ctx *context.APIContext, uid int64) { apiKeys := make([]*api.PublicKey, len(keys)) for i := range keys { 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) @@ -58,12 +103,17 @@ func ListMyPublicKeys(ctx *context.APIContext) { // swagger:operation GET /user/keys user userCurrentListKeys // --- // summary: List the authenticated user's public keys + // parameters: + // - name: fingerprint + // in: query + // description: fingerprint of the key + // type: string // produces: // - application/json // responses: // "200": // "$ref": "#/responses/PublicKeyList" - listPublicKeys(ctx, ctx.User.ID) + listPublicKeys(ctx, ctx.User) } // ListPublicKeys list the given user's public keys @@ -79,6 +129,10 @@ func ListPublicKeys(ctx *context.APIContext) { // description: username of user // type: string // required: true + // - name: fingerprint + // in: query + // description: fingerprint of the key + // type: string // responses: // "200": // "$ref": "#/responses/PublicKeyList" @@ -86,7 +140,7 @@ func ListPublicKeys(ctx *context.APIContext) { if ctx.Written() { return } - listPublicKeys(ctx, user.ID) + listPublicKeys(ctx, user) } // GetPublicKey get a public key @@ -119,7 +173,11 @@ func GetPublicKey(ctx *context.APIContext) { } 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. @@ -136,7 +194,11 @@ func CreateUserPublicKey(ctx *context.APIContext, form api.CreateKeyOption, uid return } 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 diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index efba90b18b5..56a169c295d 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -2682,6 +2682,18 @@ "name": "repo", "in": "path", "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": { @@ -4976,6 +4988,14 @@ ], "summary": "List the authenticated user's public keys", "operationId": "userCurrentListKeys", + "parameters": [ + { + "type": "string", + "description": "fingerprint of the key", + "name": "fingerprint", + "in": "query" + } + ], "responses": { "200": { "$ref": "#/responses/PublicKeyList" @@ -5540,6 +5560,12 @@ "name": "username", "in": "path", "required": true + }, + { + "type": "string", + "description": "fingerprint of the key", + "name": "fingerprint", + "in": "query" } ], "responses": {