mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-07-18 23:47:50 +00:00
feat: view authorized integration (generic)
This commit is contained in:
parent
2a0df7474a
commit
c1000624c6
14 changed files with 377 additions and 2 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <b>%s</b>",
|
||||
"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 (<code>aud</code> Claim)",
|
||||
"settings.authorized_integration.field.issuer": "Issuer (<code>iss</code> 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…",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,12 @@
|
|||
<p>{{ctx.Locale.Tr "settings.added_on" (DateUtils.AbsoluteShort .CreatedUnix)}} — {{svg "octicon-info"}} {{if .HasBeenUsed}}{{ctx.Locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="text green"{{end}}>{{DateUtils.AbsoluteShort .UpdatedUnix}}</span>{{else}}{{ctx.Locale.Tr "settings.no_activity"}}{{end}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-item-trailing">
|
||||
<a class="ui primary tiny button" href="{{AppSubUrl}}/user/settings/authorized-integrations/{{.UI}}/{{.ID}}">
|
||||
{{svg "octicon-pencil" 16 "tw-mr-1"}}
|
||||
{{ctx.Locale.Tr "settings.authorized_integration.view"}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="flex-item">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
{{template "user/settings/authorized_integrations/view_head" .}}
|
||||
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "settings.authorized_integration.claims.generic"}}
|
||||
</h4>
|
||||
<div class="ui attached bottom segment">
|
||||
<div class="field">
|
||||
<label for="issuer">{{ctx.Locale.Tr "settings.authorized_integration.field.issuer"}}</label>
|
||||
<input id="issuer" name="issuer" value="{{.Form.Issuer}}" readonly>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="content">{{ctx.Locale.Tr "settings.authorized_integration.field.claim_rules"}}</label>
|
||||
<textarea id="content" name="content" class="tw-hidden">{{.Form.ClaimRules}}</textarea>
|
||||
{{template "shared/codemirror_container" .}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{{template "user/settings/authorized_integrations/view_footer" .}}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "settings.authorized_integration.perms.title"}}
|
||||
</h4>
|
||||
<div class="ui attached bottom segment">
|
||||
|
||||
<div class="field">
|
||||
<fieldset>
|
||||
<label>{{ctx.Locale.Tr "settings.repo_and_org_access"}}</label>
|
||||
<div class="field">
|
||||
<div class="field ui radio checkbox">
|
||||
<input id="resource-all" class="enable-system" type="radio" name="resource" value="all" {{if eq .Form.Resource "all"}}checked{{end}} disabled>
|
||||
<label for="resource-all">{{ctx.Locale.Tr "settings.permissions_access_all"}}</label>
|
||||
<p class="help">{{ctx.Locale.Tr "settings.access_token.resource_all_help"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="field ui radio checkbox">
|
||||
<input id="resource-public-only" class="enable-system" type="radio" name="resource" value="public-only" {{if eq .Form.Resource "public-only"}}checked{{end}} disabled>
|
||||
<label for="resource-public-only">{{ctx.Locale.Tr "settings.permissions_public_only"}}</label>
|
||||
<p class="help">
|
||||
{{ctx.Locale.Tr "settings.access_token.resource_public_only_help"}}
|
||||
{{if $.IsAdmin}}{{ctx.Locale.Tr "settings.access_token.admin_disabled"}}{{end}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="field ui radio checkbox">
|
||||
<input id="resource-repo-specific" class="enable-system" type="radio" name="resource" value="repo-specific" {{if eq .Form.Resource "repo-specific"}}checked{{end}} disabled>
|
||||
<label for="resource-repo-specific">{{ctx.Locale.Tr "settings.permissions_access_specific_repositories"}}</label>
|
||||
<p class="help">
|
||||
{{ctx.Locale.Tr "settings.access_token.resource_specific_repo_help"}}
|
||||
{{if $.IsAdmin}}{{ctx.Locale.Tr "settings.access_token.admin_disabled"}}{{end}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="field">
|
||||
<label>
|
||||
{{ctx.Locale.Tr "settings.select_permissions"}}
|
||||
</label>
|
||||
<p class="activity meta">
|
||||
<p>{{ctx.Locale.Tr "settings.access_token_desc" (printf "%s/api/swagger" AppSubUrl) "https://forgejo.org/docs/latest/user/token-scope/"}}</p>
|
||||
</p>
|
||||
|
||||
{{range .Categories}}
|
||||
<div class="field tw-pl-1 tw-pb-1">
|
||||
<label for="scope-{{.}}">
|
||||
{{.}}
|
||||
</label>
|
||||
<div class="gitea-select">
|
||||
<select class="ui selection" name="scope" id="scope-{{.}}" disabled>
|
||||
<option value="">
|
||||
{{ctx.Locale.Tr "settings.permission_no_access"}}
|
||||
</option>
|
||||
<option value="read:{{.}}" {{if (SliceUtils.Contains $.Form.Scope (printf "read:%s" .))}} selected {{end}}>
|
||||
{{ctx.Locale.Tr "settings.permission_read"}}
|
||||
</option>
|
||||
<option value="write:{{.}}" {{if or $.Form.ScopeAll (SliceUtils.Contains $.Form.Scope (printf "write:%s" .))}} selected {{end}}>
|
||||
{{ctx.Locale.Tr "settings.permission_write"}}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{template "user/settings/layout_footer" .}}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
{{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings applications authorized-integrations")}}
|
||||
|
||||
<div class="user-setting-content">
|
||||
<form id="scoped-access-form" class="ui form" action="{{.Link}}" method="post">
|
||||
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "settings.authorized_integration.view_page_title" .Form.Name}}
|
||||
</h4>
|
||||
<div class="ui attached bottom segment">
|
||||
<div class="field">
|
||||
<label for="name">{{ctx.Locale.Tr "settings.authorized_integration.field.name"}}</label>
|
||||
<input id="name" name="name" value="{{.Form.Name}}" maxlength="255" readonly>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="description">{{ctx.Locale.Tr "settings.authorized_integration.field.description"}}</label>
|
||||
<textarea name="description" rows="5" placeholder="{{ctx.Locale.Tr "settings.authorized_integration.field.description.placeholder"}}" readonly>{{.Form.Description}}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="audience">{{ctx.Locale.Tr "settings.authorized_integration.field.audience"}}</label>
|
||||
<div class="ui fluid action input">
|
||||
<input id="audience" name="audience" value="{{.Form.Audience}}" readonly>
|
||||
<button class="ui small icon button" title="{{ctx.Locale.Tr "settings.authorized_integration.copy_audience"}}" aria-label="{{ctx.Locale.Tr "settings.authorized_integration.copy_audience"}}" type="button" data-clipboard-target="#audience">{{svg "octicon-copy" 16}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -35,6 +35,28 @@ func (doc *HTMLDoc) AssertElementPredicate(t testing.TB, selector string, predic
|
|||
predicate(selection)
|
||||
}
|
||||
|
||||
// Verify that a single element exists with the given selector, and it has the attribute `checked`.
|
||||
func (doc *HTMLDoc) AssertElementChecked(t testing.TB, selector string) {
|
||||
doc.AssertElementPredicate(t, selector, func(element *goquery.Selection) {
|
||||
if assert.Equal(t, 1, element.Length(), 1) {
|
||||
val, exists := element.Attr("checked")
|
||||
assert.True(t, exists)
|
||||
assert.Empty(t, val)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Verify that a single element exists with the given selector, and it has the attribute `selected`.
|
||||
func (doc *HTMLDoc) AssertElementSelected(t testing.TB, selector string) {
|
||||
doc.AssertElementPredicate(t, selector, func(element *goquery.Selection) {
|
||||
if assert.Equal(t, 1, element.Length(), 1) {
|
||||
val, exists := element.Attr("selected")
|
||||
assert.True(t, exists)
|
||||
assert.Empty(t, val)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Fetch attr from selector, which must exist, and pass it into the provided function which should perform test
|
||||
// assert/require checks on the attribute value.
|
||||
func (doc *HTMLDoc) AssertAttrPredicate(t testing.TB, selector, attr string, predicate func(attrValue string)) {
|
||||
|
|
|
|||
|
|
@ -852,6 +852,8 @@ func newAITester(t *testing.T, setupAI ...func(*auth.AuthorizedIntegration)) *Au
|
|||
},
|
||||
ResourceAllRepos: true,
|
||||
Name: fmt.Sprintf("AI %s", t.Name()),
|
||||
Description: fmt.Sprintf("An Authorized Integration created for the test case %s.\nIt's pretty neat.", t.Name()),
|
||||
UI: auth.AuthorizedIntegrationUIGeneric,
|
||||
}
|
||||
for _, setup := range setupAI {
|
||||
setup(ait.authorizedIntegration)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
|
@ -34,6 +35,7 @@ import (
|
|||
"github.com/pquerna/otp/totp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
func TestViewUser(t *testing.T) {
|
||||
|
|
@ -1329,3 +1331,58 @@ func TestAuthorizedIntegrationList(t *testing.T) {
|
|||
htmlDoc.AssertSelection(t, htmlDoc.FindByTextTrim("div.flex-item-body p", noAI), false) // "no ... configured" no longer present
|
||||
htmlDoc.AssertSelection(t, htmlDoc.FindByTextTrim("div.flex-item span.flex-item-title", "AI TestAuthorizedIntegrationList"), true)
|
||||
}
|
||||
|
||||
func TestAuthorizedIntegrationView(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
locale := translation.NewLocale("en-US")
|
||||
topDescription := locale.TrString("settings.authorized_integration.desc")
|
||||
noAI := locale.TrString("settings.authorized_integration.none")
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
|
||||
// Create an Authorized Integration
|
||||
ait := newAITester(t)
|
||||
defer ait.close()
|
||||
|
||||
// Load page which should now have a generic authorized integration:
|
||||
req := NewRequest(t, "GET", "/user/settings/authorized-integrations")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
htmlDoc.AssertSelection(t, htmlDoc.FindByTextTrim("div.flex-item", topDescription), true)
|
||||
htmlDoc.AssertSelection(t, htmlDoc.FindByTextTrim("div.flex-item-body p", noAI), false) // "no ... configured" no longer present
|
||||
htmlDoc.AssertSelection(t, htmlDoc.FindByTextTrim("div.flex-item span.flex-item-title", "AI TestAuthorizedIntegrationView"), true)
|
||||
|
||||
// Find button to view/edit and verify it's URL
|
||||
viewButtonSelector := htmlDoc.Find("div.flex-item-trailing a.primary.button")
|
||||
require.Equal(t, 1, viewButtonSelector.Length())
|
||||
viewButton := viewButtonSelector.Get(0)
|
||||
hrefIndex := slices.IndexFunc(viewButton.Attr, func(attr html.Attribute) bool {
|
||||
return attr.Key == "href"
|
||||
})
|
||||
require.NotEqual(t, -1, hrefIndex)
|
||||
href := viewButton.Attr[hrefIndex].Val
|
||||
assert.Equal(t, fmt.Sprintf("/user/settings/authorized-integrations/generic/%d", ait.authorizedIntegration.ID), href)
|
||||
|
||||
// Load the view/edit page
|
||||
req = NewRequest(t, "GET", href)
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc = NewHTMLParser(t, resp.Body)
|
||||
|
||||
// Assert contents of all the fields
|
||||
htmlDoc.AssertAttrEqual(t, "#name", "value", "AI TestAuthorizedIntegrationView")
|
||||
assert.Equal(t, "An Authorized Integration created for the test case TestAuthorizedIntegrationView.\nIt's pretty neat.", htmlDoc.doc.Find("textarea[name='description']").Text())
|
||||
htmlDoc.AssertAttrEqual(t, "#audience", "value", ait.authorizedIntegration.Audience)
|
||||
htmlDoc.AssertAttrEqual(t, "#issuer", "value", ait.authorizedIntegration.Issuer)
|
||||
assert.Equal(t, "{\n \"rules\": [\n {\n \"claim\": \"custom-claim\",\n \"compare\": \"eq\",\n \"value\": \"custom-claim-value\"\n }\n ]\n}", htmlDoc.doc.Find("textarea[id='content']").Text()) //nolint:testifylint // this isn't a JSON comparison; the formatting here should be exact as it represents the auto-indentation generated by the server
|
||||
htmlDoc.AssertElementChecked(t, "#resource-all")
|
||||
htmlDoc.AssertElementSelected(t, "#scope-activitypub option[value='write:activitypub']")
|
||||
assert.Equal(t, 0, htmlDoc.Find("#scope-admin").Length()) // not an admin user
|
||||
htmlDoc.AssertElementSelected(t, "#scope-issue option[value='write:issue']")
|
||||
htmlDoc.AssertElementSelected(t, "#scope-misc option[value='write:misc']")
|
||||
htmlDoc.AssertElementSelected(t, "#scope-notification option[value='write:notification']")
|
||||
htmlDoc.AssertElementSelected(t, "#scope-organization option[value='write:organization']")
|
||||
htmlDoc.AssertElementSelected(t, "#scope-package option[value='write:package']")
|
||||
htmlDoc.AssertElementSelected(t, "#scope-repository option[value='write:repository']")
|
||||
htmlDoc.AssertElementSelected(t, "#scope-user option[value='write:user']")
|
||||
}
|
||||
|
|
|
|||
7
web_src/js/features/authorized-integration.js
Normal file
7
web_src/js/features/authorized-integration.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import $ from 'jquery';
|
||||
import {createCodemirror} from './codemirror.ts';
|
||||
|
||||
export function initAuthorizedIntegrationClaimRuleEditor() {
|
||||
if (!$('.user.authorized-integrations').length) return;
|
||||
const _promise = createCodemirror($('#content')[0], 'claims.json', {language: 'JSON'});
|
||||
}
|
||||
|
|
@ -90,6 +90,7 @@ import {initRepositorySearch} from './features/repo-search.js';
|
|||
import {initColorPickers} from './features/colorpicker.js';
|
||||
import {initRepoMilestoneEditor} from './features/repo-milestone.js';
|
||||
import {initModalClose} from './modules/modal.ts';
|
||||
import {initAuthorizedIntegrationClaimRuleEditor} from './features/authorized-integration.js';
|
||||
|
||||
// Init Gitea's Fomantic settings
|
||||
initGiteaFomantic();
|
||||
|
|
@ -195,6 +196,7 @@ onDomReady(() => {
|
|||
initRepoDiffView();
|
||||
initColorPickers();
|
||||
initModalClose();
|
||||
initAuthorizedIntegrationClaimRuleEditor();
|
||||
|
||||
// Deactivate CSS-only noJS usability supplements
|
||||
document.body.classList.remove('no-js');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue