fix(api): enforce mustNotBeArchived on repo topics routes (#39260)

Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
Rathina Devan E M
2026-09-08 14:58:57 +05:30
committed by GitHub
parent df8e7db02f
commit 8b6ad49a5f
5 changed files with 54 additions and 3 deletions

View File

@@ -1595,10 +1595,10 @@ func Routes() *web.Router {
m.Get("/signing-key.pub", misc.SigningKeySSH) m.Get("/signing-key.pub", misc.SigningKeySSH)
m.Group("/topics", func() { m.Group("/topics", func() {
m.Combo("").Get(repo.ListTopics). m.Combo("").Get(repo.ListTopics).
Put(reqToken(), reqAdmin(), bind(api.RepoTopicOptions{}), repo.UpdateTopics) Put(reqToken(), reqAdmin(), mustNotBeArchived, bind(api.RepoTopicOptions{}), repo.UpdateTopics)
m.Group("/{topic}", func() { m.Group("/{topic}", func() {
m.Combo("").Put(reqToken(), repo.AddTopic). m.Combo("").Put(reqToken(), mustNotBeArchived, repo.AddTopic).
Delete(reqToken(), repo.DeleteTopic) Delete(reqToken(), mustNotBeArchived, repo.DeleteTopic)
}, reqAdmin()) }, reqAdmin())
}, reqAnyRepoReader()) }, reqAnyRepoReader())
m.Get("/issue_templates", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(), repo.GetIssueTemplates) m.Get("/issue_templates", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(), repo.GetIssueTemplates)

View File

@@ -100,6 +100,8 @@ func UpdateTopics(ctx *context.APIContext) {
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
// "422": // "422":
// "$ref": "#/responses/invalidTopicsError" // "$ref": "#/responses/invalidTopicsError"
// "423":
// "$ref": "#/responses/repoArchivedError"
form := web.GetForm[*api.RepoTopicOptions](ctx) form := web.GetForm[*api.RepoTopicOptions](ctx)
topicNames := form.Topics topicNames := form.Topics
@@ -161,6 +163,8 @@ func AddTopic(ctx *context.APIContext) {
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
// "422": // "422":
// "$ref": "#/responses/invalidTopicsError" // "$ref": "#/responses/invalidTopicsError"
// "423":
// "$ref": "#/responses/repoArchivedError"
topicName := strings.TrimSpace(strings.ToLower(ctx.PathParam("topic"))) topicName := strings.TrimSpace(strings.ToLower(ctx.PathParam("topic")))
@@ -228,6 +232,8 @@ func DeleteTopic(ctx *context.APIContext) {
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
// "422": // "422":
// "$ref": "#/responses/invalidTopicsError" // "$ref": "#/responses/invalidTopicsError"
// "423":
// "$ref": "#/responses/repoArchivedError"
topicName := strings.TrimSpace(strings.ToLower(ctx.PathParam("topic"))) topicName := strings.TrimSpace(strings.ToLower(ctx.PathParam("topic")))

View File

@@ -32751,6 +32751,9 @@
}, },
"422": { "422": {
"$ref": "#/components/responses/invalidTopicsError" "$ref": "#/components/responses/invalidTopicsError"
},
"423": {
"$ref": "#/components/responses/repoArchivedError"
} }
}, },
"summary": "Replace list of topics for a repository", "summary": "Replace list of topics for a repository",
@@ -32800,6 +32803,9 @@
}, },
"422": { "422": {
"$ref": "#/components/responses/invalidTopicsError" "$ref": "#/components/responses/invalidTopicsError"
},
"423": {
"$ref": "#/components/responses/repoArchivedError"
} }
}, },
"summary": "Delete a topic from a repository", "summary": "Delete a topic from a repository",
@@ -32847,6 +32853,9 @@
}, },
"422": { "422": {
"$ref": "#/components/responses/invalidTopicsError" "$ref": "#/components/responses/invalidTopicsError"
},
"423": {
"$ref": "#/components/responses/repoArchivedError"
} }
}, },
"summary": "Add a topic to a repository", "summary": "Add a topic to a repository",

View File

@@ -20099,6 +20099,9 @@
}, },
"422": { "422": {
"$ref": "#/responses/invalidTopicsError" "$ref": "#/responses/invalidTopicsError"
},
"423": {
"$ref": "#/responses/repoArchivedError"
} }
} }
} }
@@ -20145,6 +20148,9 @@
}, },
"422": { "422": {
"$ref": "#/responses/invalidTopicsError" "$ref": "#/responses/invalidTopicsError"
},
"423": {
"$ref": "#/responses/repoArchivedError"
} }
} }
}, },
@@ -20189,6 +20195,9 @@
}, },
"422": { "422": {
"$ref": "#/responses/invalidTopicsError" "$ref": "#/responses/invalidTopicsError"
},
"423": {
"$ref": "#/responses/repoArchivedError"
} }
} }
} }

View File

@@ -186,3 +186,30 @@ func TestAPIRepoTopic(t *testing.T) {
AddTokenAuth(token4) AddTokenAuth(token4)
MakeRequest(t, req, http.StatusForbidden) MakeRequest(t, req, http.StatusForbidden)
} }
func TestAPIRepoTopicArchived(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 30}) // owner of the archived repo51
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 51})
assert.True(t, repo.IsArchived)
token := getUserToken(t, user.Name, auth_model.AccessTokenScopeWriteRepository)
// writing topics on an archived repo must be rejected, matching the web UI
req := NewRequestf(t, "PUT", "/api/v1/repos/%s/%s/topics/%s", user.Name, repo.Name, "archivedtopic").
AddTokenAuth(token)
MakeRequest(t, req, http.StatusLocked)
req = NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/topics/%s", user.Name, repo.Name, "archivedtopic").
AddTokenAuth(token)
MakeRequest(t, req, http.StatusLocked)
req = NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/topics", user.Name, repo.Name),
&api.RepoTopicOptions{Topics: []string{"archivedtopic"}}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusLocked)
// reading topics stays allowed on an archived repo
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/topics", user.Name, repo.Name)).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
}