diff --git a/models/auth/authorized_integration.go b/models/auth/authorized_integration.go
index 72ab59286f..1f1fc52a2d 100644
--- a/models/auth/authorized_integration.go
+++ b/models/auth/authorized_integration.go
@@ -163,6 +163,17 @@ func GetAuthorizedIntegration(ctx context.Context, issuer, audience string) (*Au
return &ai, nil
}
+func GetAuthorizedIntegrationByUI(ctx context.Context, ownerID int64, aiUI AuthorizedIntegrationUI, aiID int64) (*AuthorizedIntegration, error) {
+ var ai AuthorizedIntegration
+ found, err := db.GetEngine(ctx).Where("id = ? AND user_id = ? AND ui = ?", aiID, ownerID, aiUI).Get(&ai)
+ if err != nil {
+ return nil, err
+ } else if !found {
+ return nil, util.ErrNotExist
+ }
+ return &ai, nil
+}
+
func InsertAuthorizedIntegration(ctx context.Context, ai *AuthorizedIntegration) error {
if ai.Audience != "" {
return errors.New("audience cannot be provided, and must be generated by NewAuthorizedIntegration")
@@ -236,3 +247,13 @@ func (opts ListAuthorizedIntegrationOptions) ToConds() builder.Cond {
func (opts ListAuthorizedIntegrationOptions) ToOrders() string {
return "created_unix DESC"
}
+
+func ParseAuthorizedIntegrationUI(ui string) (AuthorizedIntegrationUI, error) {
+ switch ui {
+ case string(AuthorizedIntegrationUIGeneric):
+ return AuthorizedIntegrationUIGeneric, nil
+ case string(AuthorizedIntegrationUIForgejoActionsLocal):
+ return AuthorizedIntegrationUIForgejoActionsLocal, nil
+ }
+ return AuthorizedIntegrationUI(""), fmt.Errorf("invalid authorized integration UI: %q", ui)
+}
diff --git a/models/auth/authorized_integration_test.go b/models/auth/authorized_integration_test.go
index ff6d399827..447640b38c 100644
--- a/models/auth/authorized_integration_test.go
+++ b/models/auth/authorized_integration_test.go
@@ -45,6 +45,28 @@ func TestGetAuthorizedIntegration(t *testing.T) {
assert.Equal(t, ai.ID, get.ID)
}
+func TestGetAuthorizedIntegrationByUI(t *testing.T) {
+ require.NoError(t, unittest.PrepareTestDatabase())
+ ai := makeAuthorizedIntegration(t)
+
+ get, err := auth_model.GetAuthorizedIntegrationByUI(t.Context(), 3, ai.UI, ai.ID)
+ require.ErrorIs(t, err, util.ErrNotExist)
+ assert.Nil(t, get)
+
+ get, err = auth_model.GetAuthorizedIntegrationByUI(t.Context(), ai.UserID, auth_model.AuthorizedIntegrationUI("incorrect"), ai.ID)
+ require.ErrorIs(t, err, util.ErrNotExist)
+ assert.Nil(t, get)
+
+ get, err = auth_model.GetAuthorizedIntegrationByUI(t.Context(), ai.UserID, ai.UI, -1)
+ require.ErrorIs(t, err, util.ErrNotExist)
+ assert.Nil(t, get)
+
+ get, err = auth_model.GetAuthorizedIntegrationByUI(t.Context(), ai.UserID, ai.UI, ai.ID)
+ require.NoError(t, err)
+ require.NotNil(t, get)
+ assert.Equal(t, ai.ID, get.ID)
+}
+
func TestAuthorizedIntegrationUpdateLastUsed(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
diff --git a/options/locale_next/locale_en-US.json b/options/locale_next/locale_en-US.json
index 15e234717f..f8ddb6f59c 100644
--- a/options/locale_next/locale_en-US.json
+++ b/options/locale_next/locale_en-US.json
@@ -324,6 +324,17 @@
"settings.authorized_integration.ui.generic": "Generic JWT",
"settings.authorized_integration.ui.forgejo_actions_local": "Forgejo Actions (Local)",
"settings.authorized_integration.none": "No authorized integrations currently configured.",
+ "settings.authorized_integration.view": "View",
+ "settings.authorized_integration.view_page_title": "Authorized Integration %s",
+ "settings.authorized_integration.field.name": "Name",
+ "settings.authorized_integration.field.description": "Description",
+ "settings.authorized_integration.field.description.placeholder": "Used to publish packages when ...",
+ "settings.authorized_integration.field.audience": "Audience (aud Claim)",
+ "settings.authorized_integration.field.issuer": "Issuer (iss Claim)",
+ "settings.authorized_integration.field.claim_rules": "Claim Rules JSON",
+ "settings.authorized_integration.claims.generic": "Generic JWT Rules",
+ "settings.authorized_integration.perms.title": "Capabilities Permitted",
+ "settings.authorized_integration.copy_audience": "Copy audience to clipboard",
"webauthn.insert_key": "Insert your security key",
"webauthn.sign_in": "Press the button on your security key. If your security key has no button, re-insert it.",
"webauthn.press_button": "Please press the button on your security key…",
diff --git a/routers/web/user/setting/authorized_integrations.go b/routers/web/user/setting/authorized_integrations.go
index 4268856a74..4e15c8addf 100644
--- a/routers/web/user/setting/authorized_integrations.go
+++ b/routers/web/user/setting/authorized_integrations.go
@@ -4,17 +4,22 @@
package setting
import (
+ "errors"
"net/http"
+ "slices"
auth_model "forgejo.org/models/auth"
"forgejo.org/models/db"
"forgejo.org/modules/base"
+ "forgejo.org/modules/json"
"forgejo.org/modules/optional"
+ "forgejo.org/modules/util"
"forgejo.org/services/context"
)
const (
- tplSettingsAuthorizedIntegrations base.TplName = "user/settings/authorized_integrations"
+ tplAuthorizedIntegrationList base.TplName = "user/settings/authorized_integrations"
+ tplAuthorizedIntegrationViewGeneric base.TplName = "user/settings/authorized_integrations/generic/view"
)
func ListAuthorizedIntegrations(ctx *context.Context) {
@@ -29,5 +34,101 @@ func ListAuthorizedIntegrations(ctx *context.Context) {
}
ctx.Data["AuthorizedIntegrations"] = ais
- ctx.HTML(http.StatusOK, tplSettingsAuthorizedIntegrations)
+ ctx.HTML(http.StatusOK, tplAuthorizedIntegrationList)
+}
+
+type AuthorizedIntegrationForm struct {
+ Name string
+ Description string
+ Audience string
+ Resource string // all, public-only, repo-specific
+ ScopeAll bool
+ Scope []string
+ SelectedRepo []string // slice of ownername/reponame
+
+ // Future: ClaimRules is only required when aiUI == "generic"
+ ClaimRules string
+ // Future: Issuer is likely to be replaced with more-specific fields on non-generic UIs
+ Issuer string
+}
+
+func ViewAuthorizedIntegration(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("settings.authorized_integrations")
+ ctx.Data["PageIsSettingsAuthorizedIntegrations"] = true
+
+ aiUIString := ctx.Params("ui")
+ aiUI, err := auth_model.ParseAuthorizedIntegrationUI(aiUIString)
+ if err != nil {
+ ctx.NotFound("ParseAuthorizedIntegrationUI", err)
+ return
+ }
+
+ aiID := ctx.ParamsInt64("id")
+ ai, err := auth_model.GetAuthorizedIntegrationByUI(ctx, ctx.Doer.ID, aiUI, aiID)
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.NotFound("GetAuthorizedIntegrationByUI", err)
+ return
+ } else if err != nil {
+ ctx.ServerError("GetAuthorizedIntegrationByUI", err)
+ return
+ }
+
+ form := &AuthorizedIntegrationForm{
+ Name: ai.Name,
+ Description: ai.Description,
+ Audience: ai.Audience,
+ Issuer: ai.Issuer,
+ }
+
+ if ai.ResourceAllRepos {
+ publicOnly, err := ai.Scope.PublicOnly()
+ if err != nil {
+ ctx.ServerError("PublicOnly", err)
+ return
+ }
+ if publicOnly {
+ form.Resource = "public-only"
+ } else {
+ form.Resource = "all"
+ }
+ } else {
+ form.Resource = "repo-specific"
+ }
+
+ form.Scope = ai.Scope.StringSlice()
+ form.ScopeAll, err = ai.Scope.HasScope(auth_model.AccessTokenScopeAll)
+ if err != nil {
+ ctx.ServerError("HasScope", err)
+ return
+ }
+
+ // Future: ClaimRules is only required when aiUI == "generic"
+ claimRulesJSON, err := json.MarshalIndent(ai.ClaimRules, "", " ")
+ if err != nil {
+ ctx.ServerError("MarshalIndent", err)
+ return
+ }
+ form.ClaimRules = string(claimRulesJSON)
+
+ // FIXME: form.SelectedRepo
+
+ ctx.Data["Form"] = form
+
+ categories := []string{
+ "activitypub",
+ "issue",
+ "misc",
+ "notification",
+ "organization",
+ "package",
+ "repository",
+ "user",
+ }
+ if ctx.Doer.IsAdmin {
+ categories = append(categories, "admin")
+ }
+ slices.Sort(categories)
+ ctx.Data["Categories"] = categories
+
+ ctx.HTML(http.StatusOK, tplAuthorizedIntegrationViewGeneric)
}
diff --git a/routers/web/web.go b/routers/web/web.go
index a5fc7cf720..0e0559f557 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -671,6 +671,7 @@ func registerRoutes(m *web.Route) {
})
m.Group("/authorized-integrations", func() {
+ m.Get("/{ui}/{id}", user_setting.ViewAuthorizedIntegration)
m.Get("", user_setting.ListAuthorizedIntegrations)
})
diff --git a/templates/user/settings/authorized_integrations.tmpl b/templates/user/settings/authorized_integrations.tmpl
index 3dbd2920d4..868077bbfa 100644
--- a/templates/user/settings/authorized_integrations.tmpl
+++ b/templates/user/settings/authorized_integrations.tmpl
@@ -28,6 +28,12 @@
{{ctx.Locale.Tr "settings.added_on" (DateUtils.AbsoluteShort .CreatedUnix)}} — {{svg "octicon-info"}} {{if .HasBeenUsed}}{{ctx.Locale.Tr "settings.last_used"}} {{DateUtils.AbsoluteShort .UpdatedUnix}}{{else}}{{ctx.Locale.Tr "settings.no_activity"}}{{end}}
+{{ctx.Locale.Tr "settings.access_token_desc" (printf "%s/api/swagger" AppSubUrl) "https://forgejo.org/docs/latest/user/token-scope/"}}
+ + + {{range .Categories}} +