feat: add 'ui' field to authorized_integration table

This commit is contained in:
Mathieu Fenniak
2026-05-14 23:54:16 +02:00
committed by Mathieu Fenniak
parent 21716ef31c
commit 2327b3b888
5 changed files with 77 additions and 1 deletions
@@ -120,6 +120,7 @@ func runCreateAuthorizedIntegration(ctx context.Context, c *cli.Command) error {
UserID: user.ID,
Name: c.String("name"),
Description: c.String("description"),
UI: auth_model.AuthorizedIntegrationUIGeneric,
}
var rules []auth_model.ClaimRule
+23
View File
@@ -33,6 +33,18 @@ type AuthorizedIntegration struct {
Name string // short name for lists of authorized integrations
Description string `xorm:"LONGTEXT"` // long description, optional to document relevant details of the integration
// Which UI to use for view/edit of this Authorized Integration. Authorized Integrations' functional behaviour is
// defined by other fields, such as the Issuer, Audience, ClaimRules. The UI field only defines how this record can
// be interacted with by the user in order to provide user-friendly access for specific systems -- like Forgejo
// Actions. Within the potential scope of the UI is any user interaction with the Authorized Integration -- web
// create, read, update, API, CLI.
//
// The UI field must never be used to make functional decisions about evaluating JWTs. It must always be possible
// to convert an Authorized Integration to a "generic" UI (for customization that the UI doesn't support). The
// intent of this design is that, the Authorized Integration system is complicated in its claim rules, but they
// always fully define the behaviour in a transparent manner.
UI AuthorizedIntegrationUI `xorm:"NOT NULL default('generic')"`
// Exact-match `iss` claim of the JWT
Issuer string `xorm:"NOT NULL UNIQUE(s)"`
// Exact-match `aud` claim of the JWT
@@ -128,6 +140,17 @@ const (
ClaimNested ClaimComparison = "nest" // recurse into a claim that is an map[string]any with it's own data fields
)
type AuthorizedIntegrationUI string
const (
// Generic UI which allows the user to view and edit claim rules directly to support integrations that Forgejo
// doesn't have a user-friendly UI to support.
AuthorizedIntegrationUIGeneric AuthorizedIntegrationUI = "generic"
// UI specific to Actions that are running on this local Forgejo instance accessing itself.
AuthorizedIntegrationUIForgejoActionsLocal AuthorizedIntegrationUI = "forgejo-actions-local"
)
func GetAuthorizedIntegration(ctx context.Context, issuer, audience string) (*AuthorizedIntegration, error) {
var ai AuthorizedIntegration
found, err := db.GetEngine(ctx).Where("issuer = ? AND audience = ?", issuer, audience).Get(&ai)
@@ -0,0 +1,27 @@
// Copyright 2026 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: GPL-3.0-or-later
package forgejo_migrations
import (
"xorm.io/xorm"
)
func init() {
registerMigration(&Migration{
Description: "add ui to authorized_integration",
Upgrade: addAuthorizedIntegrationUI,
})
}
func addAuthorizedIntegrationUI(x *xorm.Engine) error {
type AuthorizedIntegration struct {
UI string `xorm:"NOT NULL default('generic')"`
}
_, err := x.SyncWithOptions(
xorm.SyncOptions{IgnoreDropIndices: true},
new(AuthorizedIntegration),
)
return err
}
+10 -1
View File
@@ -5,11 +5,14 @@ package authz
import (
"context"
"errors"
"fmt"
auth_model "forgejo.org/models/auth"
)
var ErrAuthorizedIntegrationBadUI = errors.New("invalid authorized integration UI")
func GetAuthorizationReducerForAuthorizedIntegration(ctx context.Context, ai *auth_model.AuthorizedIntegration) (AuthorizationReducer, error) {
if ai.ResourceAllRepos {
if publicOnly, err := ai.Scope.PublicOnly(); err != nil {
@@ -35,6 +38,12 @@ func GetAuthorizationReducerForAuthorizedIntegration(ctx context.Context, ai *au
// Validate that an authorized integration's state is valid for creation. For example, that it doesn't have a
// conflicting set of resources (public-only and specific repositories), and other similar checks.
func ValidateAuthorizedIntegration(ai *auth_model.AuthorizedIntegration, repoResources []*auth_model.AuthorizedIntegResourceRepo) error {
// Other validations may be added here in the future.
switch ai.UI {
case auth_model.AuthorizedIntegrationUIGeneric,
auth_model.AuthorizedIntegrationUIForgejoActionsLocal:
break
default:
return fmt.Errorf("%w: invalid UI: %q", ErrAuthorizedIntegrationBadUI, ai.UI)
}
return validateRepositoryResource(ai.ResourceAllRepos, ai.Scope, len(repoResources))
}
@@ -51,6 +51,7 @@ func TestValidateAuthorizedIntegration(t *testing.T) {
ai := &auth.AuthorizedIntegration{
ResourceAllRepos: true,
Scope: auth.AccessTokenScopeReadRepository,
UI: auth.AuthorizedIntegrationUIGeneric,
}
err := ValidateAuthorizedIntegration(ai, nil)
require.NoError(t, err)
@@ -60,6 +61,7 @@ func TestValidateAuthorizedIntegration(t *testing.T) {
ai := &auth.AuthorizedIntegration{
ResourceAllRepos: false,
Scope: auth.AccessTokenScopeReadRepository,
UI: auth.AuthorizedIntegrationUIGeneric,
}
resources := []*auth.AuthorizedIntegResourceRepo{{RepoID: 12}}
err := ValidateAuthorizedIntegration(ai, resources)
@@ -70,6 +72,7 @@ func TestValidateAuthorizedIntegration(t *testing.T) {
ai := &auth.AuthorizedIntegration{
ResourceAllRepos: false,
Scope: auth.AccessTokenScopeReadRepository,
UI: auth.AuthorizedIntegrationUIGeneric,
}
resources := []*auth.AuthorizedIntegResourceRepo{}
err := ValidateAuthorizedIntegration(ai, resources)
@@ -80,6 +83,7 @@ func TestValidateAuthorizedIntegration(t *testing.T) {
ai := &auth.AuthorizedIntegration{
ResourceAllRepos: false,
Scope: auth.AccessTokenScope(strings.Join([]string{string(auth.AccessTokenScopePublicOnly), string(auth.AccessTokenScopeReadRepository)}, ",")),
UI: auth.AuthorizedIntegrationUIGeneric,
}
resources := []*auth.AuthorizedIntegResourceRepo{{RepoID: 12}}
err := ValidateAuthorizedIntegration(ai, resources)
@@ -90,10 +94,22 @@ func TestValidateAuthorizedIntegration(t *testing.T) {
ai := &auth.AuthorizedIntegration{
ResourceAllRepos: false,
Scope: auth.AccessTokenScopeReadAdmin,
UI: auth.AuthorizedIntegrationUIGeneric,
}
resources := []*auth.AuthorizedIntegResourceRepo{{RepoID: 12}}
err := ValidateAuthorizedIntegration(ai, resources)
require.ErrorIs(t, err, ErrSpecifiedReposInvalidScope)
require.ErrorContains(t, err, string(auth.AccessTokenScopeReadAdmin))
})
t.Run("invalid - missing UI", func(t *testing.T) {
ai := &auth.AuthorizedIntegration{
ResourceAllRepos: false,
Scope: auth.AccessTokenScopeReadAdmin,
}
resources := []*auth.AuthorizedIntegResourceRepo{{RepoID: 12}}
err := ValidateAuthorizedIntegration(ai, resources)
require.ErrorIs(t, err, ErrAuthorizedIntegrationBadUI)
require.ErrorContains(t, err, "invalid UI: \"\"")
})
}