feat: add 'ui' field to authorized_integration table
This commit is contained in:
committed by
Mathieu Fenniak
parent
21716ef31c
commit
2327b3b888
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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: \"\"")
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user