feat: add licensing API token scope #698

Merged
jmiller merged 1 commits from feature/api-token-scopes into main 2026-06-25 14:37:50 +00:00
4 changed files with 24 additions and 6 deletions
+1
View File
@@ -3,6 +3,7 @@
## [Unreleased]
### Added
- API token scope `read:licensing` / `write:licensing` for licensing endpoints (#697)
- Wiki full-text search: case-insensitive search across all wiki page titles and content (#550)
- Wiki search API: GET /wiki/search?q=term with paginated JSON results (#550)
- Metadata deploy fields: deploy_host, deploy_port, deploy_user, deploy_path, docker_image, docker_registry, container_name, health_url (#692)
+16 -2
View File
@@ -24,6 +24,7 @@ const (
AccessTokenScopeCategoryIssue
AccessTokenScopeCategoryRepository
AccessTokenScopeCategoryUser
AccessTokenScopeCategoryLicensing
)
// AllAccessTokenScopeCategories contains all access token scope categories
@@ -37,6 +38,7 @@ var AllAccessTokenScopeCategories = []AccessTokenScopeCategory{
AccessTokenScopeCategoryIssue,
AccessTokenScopeCategoryRepository,
AccessTokenScopeCategoryUser,
AccessTokenScopeCategoryLicensing,
}
// AccessTokenScopeLevel represents the access levels without a given scope category
@@ -82,6 +84,9 @@ const (
AccessTokenScopeReadUser AccessTokenScope = "read:user"
AccessTokenScopeWriteUser AccessTokenScope = "write:user"
AccessTokenScopeReadLicensing AccessTokenScope = "read:licensing"
AccessTokenScopeWriteLicensing AccessTokenScope = "write:licensing"
)
// accessTokenScopeBitmap represents a bitmap of access token scopes.
@@ -93,7 +98,8 @@ const (
accessTokenScopeAllBits accessTokenScopeBitmap = accessTokenScopeWriteActivityPubBits |
accessTokenScopeWriteAdminBits | accessTokenScopeWriteMiscBits | accessTokenScopeWriteNotificationBits |
accessTokenScopeWriteOrganizationBits | accessTokenScopeWritePackageBits | accessTokenScopeWriteIssueBits |
accessTokenScopeWriteRepositoryBits | accessTokenScopeWriteUserBits
accessTokenScopeWriteRepositoryBits | accessTokenScopeWriteUserBits |
accessTokenScopeWriteLicensingBits
accessTokenScopePublicOnlyBits accessTokenScopeBitmap = 1 << iota
@@ -124,6 +130,9 @@ const (
accessTokenScopeReadUserBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWriteUserBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadUserBits
accessTokenScopeReadLicensingBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWriteLicensingBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadLicensingBits
// The current implementation only supports up to 64 token scopes.
// If we need to support > 64 scopes,
// refactoring the whole implementation in this file (and only this file) is needed.
@@ -142,6 +151,7 @@ var allAccessTokenScopes = []AccessTokenScope{
AccessTokenScopeWriteIssue, AccessTokenScopeReadIssue,
AccessTokenScopeWriteRepository, AccessTokenScopeReadRepository,
AccessTokenScopeWriteUser, AccessTokenScopeReadUser,
AccessTokenScopeWriteLicensing, AccessTokenScopeReadLicensing,
}
// allAccessTokenScopeBits contains all access token scopes.
@@ -166,6 +176,8 @@ var allAccessTokenScopeBits = map[AccessTokenScope]accessTokenScopeBitmap{
AccessTokenScopeWriteRepository: accessTokenScopeWriteRepositoryBits,
AccessTokenScopeReadUser: accessTokenScopeReadUserBits,
AccessTokenScopeWriteUser: accessTokenScopeWriteUserBits,
AccessTokenScopeReadLicensing: accessTokenScopeReadLicensingBits,
AccessTokenScopeWriteLicensing: accessTokenScopeWriteLicensingBits,
}
// readAccessTokenScopes maps a scope category to the read permission scope
@@ -180,6 +192,7 @@ var accessTokenScopes = map[AccessTokenScopeLevel]map[AccessTokenScopeCategory]A
AccessTokenScopeCategoryIssue: AccessTokenScopeReadIssue,
AccessTokenScopeCategoryRepository: AccessTokenScopeReadRepository,
AccessTokenScopeCategoryUser: AccessTokenScopeReadUser,
AccessTokenScopeCategoryLicensing: AccessTokenScopeReadLicensing,
},
Write: {
AccessTokenScopeCategoryActivityPub: AccessTokenScopeWriteActivityPub,
@@ -191,6 +204,7 @@ var accessTokenScopes = map[AccessTokenScopeLevel]map[AccessTokenScopeCategory]A
AccessTokenScopeCategoryIssue: AccessTokenScopeWriteIssue,
AccessTokenScopeCategoryRepository: AccessTokenScopeWriteRepository,
AccessTokenScopeCategoryUser: AccessTokenScopeWriteUser,
AccessTokenScopeCategoryLicensing: AccessTokenScopeWriteLicensing,
},
}
@@ -370,7 +384,7 @@ func (bitmap accessTokenScopeBitmap) toScope() AccessTokenScope {
scope := AccessTokenScope(strings.Join(scopes, ","))
scope = AccessTokenScope(strings.ReplaceAll(
string(scope),
"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user",
"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user,write:licensing",
"all",
))
return scope
+3 -3
View File
@@ -17,13 +17,13 @@ type scopeTestNormalize struct {
}
func TestAccessTokenScope_Normalize(t *testing.T) {
assert.Equal(t, []string{"activitypub", "admin", "issue", "misc", "notification", "organization", "package", "repository", "user"}, GetAccessTokenCategories())
assert.Equal(t, []string{"activitypub", "admin", "issue", "licensing", "misc", "notification", "organization", "package", "repository", "user"}, GetAccessTokenCategories())
tests := []scopeTestNormalize{
{"", "", nil},
{"write:misc,write:notification,read:package,write:notification,public-only", "public-only,write:misc,write:notification,read:package", nil},
{"all", "all", nil},
{"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user", "all", nil},
{"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user,public-only", "public-only,all", nil},
{"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user,write:licensing", "all", nil},
{"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user,write:licensing,public-only", "public-only,all", nil},
}
for _, scope := range GetAccessTokenCategories() {
+4 -1
View File
@@ -294,6 +294,9 @@ func checkTokenPublicOnly() func(ctx *context.APIContext) {
ctx.APIError(http.StatusForbidden, "token scope is limited to public packages")
return
}
case auth_model.AccessTokenScopeCategoryLicensing:
ctx.APIError(http.StatusForbidden, "token scope is limited to public resources, licensing is not available")
return
}
}
}
@@ -1892,7 +1895,7 @@ func Routes() *web.Router {
// Authenticated license detail
m.Get("/{dlid}/status", reqToken(), licensing.Status)
})
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryLicensing))
}, sudo())
return m