mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-05-12 22:10:25 +00:00
feat: allow Forgejo Actions to be used an Authorized Integration in-memory with internal issuer (#12364)
Allow JWTs that are generated by Forgejo Actions to be validated within Forgejo in-memory. Without any special support for this internal access situation, these problems would occur: 1. Forgejo would need to make an HTTP request to itself to get the valid public key for the JWT, in order to validate its signature. This is a waste of resources, and introduces a self-DoS risk. 2. Forgejo would need to be available via TLS in order for Actions to make service calls to Forgejo with that JWT, due to the TLS requirement for public key fetching. This would be a blocker for writing end-to-end tests for Forgejo, but also would affect users who do not host Forgejo with TLS. 3. Authorized Integrations would need to be saved with the `issuer` URL of Forgejo. If Forgejo's own `setting.AppURL` changed, all the persisted records in the database would become incorrect. ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. All work and communication must conform to Forgejo's [AI Agreement](https://codeberg.org/forgejo/governance/src/branch/main/AIAgreement.md). There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests for Go changes - I added test coverage for Go changes... - [x] in their respective `*_test.go` for unit tests. - [x] in the `tests/integration` directory if it involves interactions with a live Forgejo server. - I ran... - [x] `make pr-go` before pushing ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - [x] I did not document these changes and I do not expect someone else to do it. ### Release notes - [ ] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change. - [x] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/12364 Reviewed-by: Andreas Ahlenstorf <aahlenst@noreply.codeberg.org>
This commit is contained in:
parent
67250869d3
commit
7fc236c589
7 changed files with 355 additions and 4 deletions
|
|
@ -4,6 +4,7 @@ packages:
|
||||||
forgejo.org/modules/nosql:
|
forgejo.org/modules/nosql:
|
||||||
config:
|
config:
|
||||||
filename: mocks.go # make mocks public so that external packages can use
|
filename: mocks.go # make mocks public so that external packages can use
|
||||||
|
forgejo.org/services/auth/method:
|
||||||
forgejo.org/services/authz:
|
forgejo.org/services/authz:
|
||||||
config:
|
config:
|
||||||
filename: authorization_reducer_mock.go # make mocks public so that external packages can use
|
filename: authorization_reducer_mock.go # make mocks public so that external packages can use
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,14 @@ import (
|
||||||
|
|
||||||
func microcmdUserCreateAuthorizedIntegration() *cli.Command {
|
func microcmdUserCreateAuthorizedIntegration() *cli.Command {
|
||||||
return &cli.Command{
|
return &cli.Command{
|
||||||
Name: "create-authorized-integration",
|
Name: "create-authorized-integration",
|
||||||
Usage: "Create an authorized integration for a specific user",
|
Description: `Creates an authorized integration. Authorized integrations allow Forgejo to
|
||||||
|
receive JWTs from external sources, validate their claims against
|
||||||
|
user-defined rules, and grant access to Forgejo's API on behalf of a user.
|
||||||
|
|
||||||
|
The issuer may be set to "urn:forgejo:authorized-integrations:actions"
|
||||||
|
to support JWTs from the local instance's Forgejo Actions, utilizing the
|
||||||
|
enable-openid-connect flag in a workflow.`,
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "username",
|
Name: "username",
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"forgejo.org/modules/web"
|
"forgejo.org/modules/web"
|
||||||
web_types "forgejo.org/modules/web/types"
|
web_types "forgejo.org/modules/web/types"
|
||||||
actions_service "forgejo.org/services/actions"
|
actions_service "forgejo.org/services/actions"
|
||||||
|
auth_method "forgejo.org/services/auth/method"
|
||||||
"forgejo.org/services/context"
|
"forgejo.org/services/context"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -66,6 +67,7 @@ func init() {
|
||||||
web.RegisterResponseStatusProvider[*OIDCContext](func(req *http.Request) web_types.ResponseStatusProvider {
|
web.RegisterResponseStatusProvider[*OIDCContext](func(req *http.Request) web_types.ResponseStatusProvider {
|
||||||
return req.Context().Value(oidcContextKey).(*OIDCContext)
|
return req.Context().Value(oidcContextKey).(*OIDCContext)
|
||||||
})
|
})
|
||||||
|
auth_method.RegisterInternalIssuer("api/actions", internalIssuer{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func OIDCContexter() func(next http.Handler) http.Handler {
|
func OIDCContexter() func(next http.Handler) http.Handler {
|
||||||
|
|
@ -143,3 +145,16 @@ func (o *oidcRoutes) configuration(ctx *OIDCContext) {
|
||||||
func (o *oidcRoutes) keys(ctx *OIDCContext) {
|
func (o *oidcRoutes) keys(ctx *OIDCContext) {
|
||||||
ctx.JSON(http.StatusOK, o.jwks)
|
ctx.JSON(http.StatusOK, o.jwks)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register Actions OIDC as an internal issuer for authorized integrations. This allows an authorized integration to
|
||||||
|
// have the value `urn:forgejo:authorized-integrations:actions` as an issuer, and perform JWT signature validation
|
||||||
|
// against the in-memory signing key defined in this module.
|
||||||
|
type internalIssuer struct{}
|
||||||
|
|
||||||
|
func (internalIssuer) IssuerPlaceholder() string {
|
||||||
|
return "urn:forgejo:authorized-integrations:actions"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (internalIssuer) SigningKey() jwtx.SigningKey {
|
||||||
|
return jwtSigningKey
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -48,8 +49,40 @@ var (
|
||||||
return aiHTTPClient
|
return aiHTTPClient
|
||||||
}
|
}
|
||||||
getCache = cache.GetCache
|
getCache = cache.GetCache
|
||||||
|
|
||||||
|
internalIssuers = make(map[string]InternalIssuer)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Authorized Integrations can verify the signature of JWTs that the application itself generated without requiring
|
||||||
|
// remote access, and in a manner that is flexible to changes in [setting.AppURL].
|
||||||
|
//
|
||||||
|
// For example, Forgejo Actions is often used to access Forgejo with a JWT, by setting `enable-openid-connect: true` in
|
||||||
|
// a workflow. Without any special support for this internal access situation, problems would occur:
|
||||||
|
//
|
||||||
|
// 1. Forgejo would need to make an HTTP request to itself to get the valid public key for the JWT, in order to validate
|
||||||
|
// its signature. This is a waste of resources, and introduces a self-DoS risk.
|
||||||
|
//
|
||||||
|
// 2. Forgejo would need to be available via TLS in order for Actions to make service calls to Forgejo with that JWT
|
||||||
|
// (due to the TLS requirement for public key fetching).
|
||||||
|
//
|
||||||
|
// 3. Authorized Integrations would need to be saved with the `issuer` URL of Forgejo. If Forgejo's own
|
||||||
|
// [setting.AppURL] changed, all the persisted records in the database would become incorrect.
|
||||||
|
//
|
||||||
|
// Internal Issuers work by registering a URL suffix like "/api/actions". When a JWT is received with an issuer
|
||||||
|
// matching [setting.AppURL] and the registered URL suffix, then the [InternalIssuer] interface is used to access the
|
||||||
|
// JWT public key, and the value to be saved in the Authorized Integrations table as the issuer.
|
||||||
|
func RegisterInternalIssuer(urlSuffix string, internalIssuer InternalIssuer) {
|
||||||
|
internalIssuers[urlSuffix] = internalIssuer
|
||||||
|
}
|
||||||
|
|
||||||
|
//mockery:generate: true
|
||||||
|
type InternalIssuer interface {
|
||||||
|
// Signing key used to validate a JWT from this internal issuer.
|
||||||
|
SigningKey() jwtx.SigningKey
|
||||||
|
// Value to store in [auth_model.AuthorizedIntegration]'s Issuer field to reflect the use of this internal issuer.
|
||||||
|
IssuerPlaceholder() string
|
||||||
|
}
|
||||||
|
|
||||||
// Restrict document size to prevent resource exhaustion attack with a malicious authorized integration; largest
|
// Restrict document size to prevent resource exhaustion attack with a malicious authorized integration; largest
|
||||||
// real-world openid-configuration observed is about 1kB, largest JWKS is 6kB, so for both cases 16kB should be
|
// real-world openid-configuration observed is about 1kB, largest JWKS is 6kB, so for both cases 16kB should be
|
||||||
// sufficient. If this needs to change in the future, it could be moved to a config setting -- but until a reason comes
|
// sufficient. If this needs to change in the future, it could be moved to a config setting -- but until a reason comes
|
||||||
|
|
@ -111,7 +144,19 @@ func (a *AuthorizedIntegration) Verify(req *http.Request, w http.ResponseWriter,
|
||||||
return nil, fmt.Errorf("invalid `aud` claim: %q", audience)
|
return nil, fmt.Errorf("invalid `aud` claim: %q", audience)
|
||||||
}
|
}
|
||||||
|
|
||||||
authorizedIntegration, err = auth_model.GetAuthorizedIntegration(req.Context(), issuer, audience)
|
// Check if there's an internal issuer that matches the JWT's issuer, and if so, change `queryIssuer` to the
|
||||||
|
// internal issuer's placeholder, and store `internalIssuer` for later:
|
||||||
|
queryIssuer := issuer
|
||||||
|
var internalIssuer InternalIssuer
|
||||||
|
issuerSuffix := strings.TrimPrefix(issuer, setting.AppURL)
|
||||||
|
if issuer != issuerSuffix { // TrimPrefix will return a different string when the prefix was present
|
||||||
|
if ii, ok := internalIssuers[issuerSuffix]; ok {
|
||||||
|
internalIssuer = ii
|
||||||
|
queryIssuer = internalIssuer.IssuerPlaceholder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
authorizedIntegration, err = auth_model.GetAuthorizedIntegration(req.Context(), queryIssuer, audience)
|
||||||
if errors.Is(err, util.ErrNotExist) {
|
if errors.Is(err, util.ErrNotExist) {
|
||||||
return nil, errors.New("matching authorized_integration not found")
|
return nil, errors.New("matching authorized_integration not found")
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
|
|
@ -125,6 +170,14 @@ func (a *AuthorizedIntegration) Verify(req *http.Request, w http.ResponseWriter,
|
||||||
return nil, fmt.Errorf("claim mismatch: %w", err)
|
return nil, fmt.Errorf("claim mismatch: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If an internal issuer was found earlier, then we can skip the JWKS fetch and just use its in-memory
|
||||||
|
// signing key to validate the JWT. It is critical we do this after the `checkClaims` above so that we
|
||||||
|
// don't miss important validation of the JWT.
|
||||||
|
if internalIssuer != nil {
|
||||||
|
key := internalIssuer.SigningKey().VerifyKey()
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
issuerURL, err := url.Parse(issuer)
|
issuerURL, err := url.Parse(issuer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed parsing issuer: %w", err)
|
return nil, fmt.Errorf("failed parsing issuer: %w", err)
|
||||||
|
|
|
||||||
|
|
@ -676,6 +676,89 @@ func TestAuthorizedIntegration(t *testing.T) {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("internal issuer", func(t *testing.T) {
|
||||||
|
t.Run("success", func(t *testing.T) {
|
||||||
|
ait := newInternalIssuerAITester(t)
|
||||||
|
defer ait.close()
|
||||||
|
output := ait.bearerRequest()
|
||||||
|
requireOutput[*auth.AuthenticationSuccess](t, output)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("mismatched issuer app URL", func(t *testing.T) {
|
||||||
|
ait := newInternalIssuerAITester(t,
|
||||||
|
claimTweak(func(rc *flexibleClaims) {
|
||||||
|
rc.Issuer = "https://example.org/fake-jwt-issuer" // correct suffix, incorrect prefix
|
||||||
|
}))
|
||||||
|
defer ait.close()
|
||||||
|
output := ait.bearerRequest()
|
||||||
|
err := requireOutput[*auth.AuthenticationAttemptedIncorrectCredential](t, output).Error
|
||||||
|
require.ErrorContains(t, err, "matching authorized_integration not found")
|
||||||
|
ait.ii.ExpectedCalls = nil // InternalIssuer should have zero calls
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("mismatched issuer URL suffix", func(t *testing.T) {
|
||||||
|
ait := newInternalIssuerAITester(t,
|
||||||
|
claimTweak(func(rc *flexibleClaims) {
|
||||||
|
rc.Issuer = setting.AppURL + "/fake-jwt-issuer-123" // correct prefix, incorrect suffix
|
||||||
|
}))
|
||||||
|
defer ait.close()
|
||||||
|
output := ait.bearerRequest()
|
||||||
|
err := requireOutput[*auth.AuthenticationAttemptedIncorrectCredential](t, output).Error
|
||||||
|
require.ErrorContains(t, err, "matching authorized_integration not found")
|
||||||
|
ait.ii.ExpectedCalls = nil // InternalIssuer should have zero calls
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("mismatched DB issuer placeholder", func(t *testing.T) {
|
||||||
|
ait := newInternalIssuerAITester(t,
|
||||||
|
aiDBTweak(func(ai *auth_model.AuthorizedIntegration) {
|
||||||
|
ai.Issuer = "urn:forgejo:authorized-issuer:internal:bad-choice-here"
|
||||||
|
}))
|
||||||
|
defer ait.close()
|
||||||
|
output := ait.bearerRequest()
|
||||||
|
err := requireOutput[*auth.AuthenticationAttemptedIncorrectCredential](t, output).Error
|
||||||
|
require.ErrorContains(t, err, "matching authorized_integration not found")
|
||||||
|
ait.ii.ExpectedCalls = nil // InternalIssuer should have zero calls
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("checks claim rules", func(t *testing.T) {
|
||||||
|
ait := newInternalIssuerAITester(t,
|
||||||
|
claimTweak(func(rc *flexibleClaims) {
|
||||||
|
rc.other["custom-claim"] = "oops wrong claim"
|
||||||
|
}))
|
||||||
|
defer ait.close()
|
||||||
|
output := ait.bearerRequest()
|
||||||
|
err := requireOutput[*auth.AuthenticationAttemptedIncorrectCredential](t, output).Error
|
||||||
|
require.ErrorContains(t, err, "claim \"custom-claim\" must be \"custom-claim-value\"")
|
||||||
|
ait.ii.ExpectedCalls = []*mock.Call{ait.ii.ExpectedCalls[0]} // drop call to SigningKey() -- won't occur due to claim mismatch
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("JWT times checked", func(t *testing.T) {
|
||||||
|
ait := newInternalIssuerAITester(t,
|
||||||
|
claimTweak(func(rc *flexibleClaims) {
|
||||||
|
rc.ExpiresAt = jwt.NewNumericDate(time.Date(2026, time.January, 1, 12, 0, 0, 0, time.Local))
|
||||||
|
}))
|
||||||
|
defer ait.close()
|
||||||
|
output := ait.bearerRequest()
|
||||||
|
err := requireOutput[*auth.AuthenticationAttemptedIncorrectCredential](t, output).Error
|
||||||
|
require.ErrorContains(t, err, "token is expired")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("signed by incorrect JWT key", func(t *testing.T) {
|
||||||
|
keyPath := filepath.Join(t.TempDir(), "jwt-rsa-2048-bad-key.priv")
|
||||||
|
badSigningKey, err := jwtx.InitAsymmetricSigningKey(keyPath, "RS256")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ait := newInternalIssuerAITester(t, jwtClientSignatureTweak(func() jwtx.SigningKey {
|
||||||
|
return badSigningKey
|
||||||
|
}))
|
||||||
|
defer ait.close()
|
||||||
|
|
||||||
|
output := ait.bearerRequest()
|
||||||
|
err = requireOutput[*auth.AuthenticationAttemptedIncorrectCredential](t, output).Error
|
||||||
|
require.ErrorContains(t, err, "crypto/rsa: verification error")
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthorizedIntegrationTester struct {
|
type AuthorizedIntegrationTester struct {
|
||||||
|
|
@ -686,6 +769,7 @@ type AuthorizedIntegrationTester struct {
|
||||||
testServer *httptest.Server
|
testServer *httptest.Server
|
||||||
resetHTTPClient func()
|
resetHTTPClient func()
|
||||||
tweaks []tweak
|
tweaks []tweak
|
||||||
|
ii *MockInternalIssuer
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAITester(t *testing.T, tweaks ...tweak) *AuthorizedIntegrationTester {
|
func newAITester(t *testing.T, tweaks ...tweak) *AuthorizedIntegrationTester {
|
||||||
|
|
@ -788,6 +872,25 @@ func newAITester(t *testing.T, tweaks ...tweak) *AuthorizedIntegrationTester {
|
||||||
return ait
|
return ait
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newInternalIssuerAITester(t *testing.T, tweaks ...tweak) *AuthorizedIntegrationTester {
|
||||||
|
innerTweaks := []tweak{
|
||||||
|
claimTweak(func(rc *flexibleClaims) {
|
||||||
|
rc.Issuer = setting.AppURL + "/fake-jwt-issuer"
|
||||||
|
}),
|
||||||
|
aiDBTweak(func(ai *auth_model.AuthorizedIntegration) {
|
||||||
|
ai.Issuer = "urn:forgejo:authorized-issuer:internal:test1"
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
innerTweaks = append(innerTweaks, tweaks...)
|
||||||
|
ait := newAITester(t, innerTweaks...)
|
||||||
|
ii := NewMockInternalIssuer(t)
|
||||||
|
internalIssuers["/fake-jwt-issuer"] = ii
|
||||||
|
ii.On("IssuerPlaceholder").Return("urn:forgejo:authorized-issuer:internal:test1")
|
||||||
|
ii.On("SigningKey").Return(ait.jwtSigningKey)
|
||||||
|
ait.ii = ii
|
||||||
|
return ait
|
||||||
|
}
|
||||||
|
|
||||||
func (ait *AuthorizedIntegrationTester) signedJWT() string {
|
func (ait *AuthorizedIntegrationTester) signedJWT() string {
|
||||||
claims := flexibleClaims{
|
claims := flexibleClaims{
|
||||||
RegisteredClaims: jwt.RegisteredClaims{
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
|
@ -803,7 +906,13 @@ func (ait *AuthorizedIntegrationTester) signedJWT() string {
|
||||||
tweak(&claims)
|
tweak(&claims)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
signedToken, err := ait.jwtSigningKey.JWT(claims)
|
clientSigningKey := ait.jwtSigningKey
|
||||||
|
for _, tweak := range ait.tweaks {
|
||||||
|
if tweak, is := tweak.(jwtClientSignatureTweak); is {
|
||||||
|
clientSigningKey = tweak()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
signedToken, err := clientSigningKey.JWT(claims)
|
||||||
require.NoError(ait.t, err)
|
require.NoError(ait.t, err)
|
||||||
return signedToken
|
return signedToken
|
||||||
}
|
}
|
||||||
|
|
@ -840,3 +949,5 @@ type jwksTweak func(*openIDKeys)
|
||||||
type aiDBTweak func(*auth_model.AuthorizedIntegration)
|
type aiDBTweak func(*auth_model.AuthorizedIntegration)
|
||||||
|
|
||||||
type jwtxKeyTweak func() jwtx.SigningKey
|
type jwtxKeyTweak func() jwtx.SigningKey
|
||||||
|
|
||||||
|
type jwtClientSignatureTweak func() jwtx.SigningKey
|
||||||
|
|
|
||||||
129
services/auth/method/mocks_test.go
Normal file
129
services/auth/method/mocks_test.go
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
// Code generated by mockery; DO NOT EDIT.
|
||||||
|
// github.com/vektra/mockery
|
||||||
|
// template: testify
|
||||||
|
|
||||||
|
package method
|
||||||
|
|
||||||
|
import (
|
||||||
|
"forgejo.org/modules/jwtx"
|
||||||
|
|
||||||
|
mock "github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewMockInternalIssuer creates a new instance of MockInternalIssuer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||||
|
// The first argument is typically a *testing.T value.
|
||||||
|
func NewMockInternalIssuer(t interface {
|
||||||
|
mock.TestingT
|
||||||
|
Cleanup(func())
|
||||||
|
},
|
||||||
|
) *MockInternalIssuer {
|
||||||
|
mock := &MockInternalIssuer{}
|
||||||
|
mock.Mock.Test(t)
|
||||||
|
|
||||||
|
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||||
|
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockInternalIssuer is an autogenerated mock type for the InternalIssuer type
|
||||||
|
type MockInternalIssuer struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockInternalIssuer_Expecter struct {
|
||||||
|
mock *mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_m *MockInternalIssuer) EXPECT() *MockInternalIssuer_Expecter {
|
||||||
|
return &MockInternalIssuer_Expecter{mock: &_m.Mock}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IssuerPlaceholder provides a mock function for the type MockInternalIssuer
|
||||||
|
func (_mock *MockInternalIssuer) IssuerPlaceholder() string {
|
||||||
|
ret := _mock.Called()
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for IssuerPlaceholder")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 string
|
||||||
|
if returnFunc, ok := ret.Get(0).(func() string); ok {
|
||||||
|
r0 = returnFunc()
|
||||||
|
} else {
|
||||||
|
r0 = ret.Get(0).(string)
|
||||||
|
}
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockInternalIssuer_IssuerPlaceholder_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IssuerPlaceholder'
|
||||||
|
type MockInternalIssuer_IssuerPlaceholder_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// IssuerPlaceholder is a helper method to define mock.On call
|
||||||
|
func (_e *MockInternalIssuer_Expecter) IssuerPlaceholder() *MockInternalIssuer_IssuerPlaceholder_Call {
|
||||||
|
return &MockInternalIssuer_IssuerPlaceholder_Call{Call: _e.mock.On("IssuerPlaceholder")}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockInternalIssuer_IssuerPlaceholder_Call) Run(run func()) *MockInternalIssuer_IssuerPlaceholder_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run()
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockInternalIssuer_IssuerPlaceholder_Call) Return(s string) *MockInternalIssuer_IssuerPlaceholder_Call {
|
||||||
|
_c.Call.Return(s)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockInternalIssuer_IssuerPlaceholder_Call) RunAndReturn(run func() string) *MockInternalIssuer_IssuerPlaceholder_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SigningKey provides a mock function for the type MockInternalIssuer
|
||||||
|
func (_mock *MockInternalIssuer) SigningKey() jwtx.SigningKey {
|
||||||
|
ret := _mock.Called()
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for SigningKey")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 jwtx.SigningKey
|
||||||
|
if returnFunc, ok := ret.Get(0).(func() jwtx.SigningKey); ok {
|
||||||
|
r0 = returnFunc()
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(jwtx.SigningKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockInternalIssuer_SigningKey_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SigningKey'
|
||||||
|
type MockInternalIssuer_SigningKey_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// SigningKey is a helper method to define mock.On call
|
||||||
|
func (_e *MockInternalIssuer_Expecter) SigningKey() *MockInternalIssuer_SigningKey_Call {
|
||||||
|
return &MockInternalIssuer_SigningKey_Call{Call: _e.mock.On("SigningKey")}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockInternalIssuer_SigningKey_Call) Run(run func()) *MockInternalIssuer_SigningKey_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run()
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockInternalIssuer_SigningKey_Call) Return(signingKey jwtx.SigningKey) *MockInternalIssuer_SigningKey_Call {
|
||||||
|
_c.Call.Return(signingKey)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockInternalIssuer_SigningKey_Call) RunAndReturn(run func() jwtx.SigningKey) *MockInternalIssuer_SigningKey_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
@ -6,13 +6,16 @@ package integration
|
||||||
import (
|
import (
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
"math/big"
|
"math/big"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
actions_model "forgejo.org/models/actions"
|
actions_model "forgejo.org/models/actions"
|
||||||
|
"forgejo.org/models/auth"
|
||||||
"forgejo.org/models/db"
|
"forgejo.org/models/db"
|
||||||
"forgejo.org/modules/setting"
|
"forgejo.org/modules/setting"
|
||||||
|
api "forgejo.org/modules/structs"
|
||||||
actions_service "forgejo.org/services/actions"
|
actions_service "forgejo.org/services/actions"
|
||||||
"forgejo.org/tests"
|
"forgejo.org/tests"
|
||||||
|
|
||||||
|
|
@ -188,4 +191,37 @@ func TestActionsIDToken(t *testing.T) {
|
||||||
resp = MakeRequest(t, req, http.StatusBadRequest)
|
resp = MakeRequest(t, req, http.StatusBadRequest)
|
||||||
assert.Contains(t, resp.Body.String(), "run-id does not match")
|
assert.Contains(t, resp.Body.String(), "run-id does not match")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("authorized integration internal issuer", func(t *testing.T) {
|
||||||
|
// Create an Authorized Integration which is set-up to be validated with the in-memory Actions' JWT signing key:
|
||||||
|
ai := &auth.AuthorizedIntegration{
|
||||||
|
UserID: 2,
|
||||||
|
Scope: auth.AccessTokenScopeAll,
|
||||||
|
Issuer: "urn:forgejo:authorized-integrations:actions",
|
||||||
|
ClaimRules: &auth.ClaimRules{
|
||||||
|
Rules: []auth.ClaimRule{
|
||||||
|
{
|
||||||
|
Claim: "sub",
|
||||||
|
Comparison: auth.ClaimEqual,
|
||||||
|
Value: "repo:user5/repo4:ref:refs/heads/master",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ResourceAllRepos: true,
|
||||||
|
}
|
||||||
|
require.NoError(t, auth.InsertAuthorizedIntegration(t.Context(), ai))
|
||||||
|
|
||||||
|
// Create a JWT from the Actions system:
|
||||||
|
var getResponse getTokenResponse
|
||||||
|
req = NewRequest(t, "GET", fmt.Sprintf("/api/actions/_apis/pipelines/workflows/792/idtoken?placeholder=true&audience=%s", ai.Audience)).AddTokenAuth(token)
|
||||||
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
|
DecodeJSON(t, resp, &getResponse)
|
||||||
|
|
||||||
|
// Should be able to make a Forgejo API call with the JWT, authenticated by the Authorized Integration:
|
||||||
|
req := NewRequest(t, "GET", "/api/v1/user").AddTokenAuth(getResponse.Value)
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
var user api.User
|
||||||
|
DecodeJSON(t, resp, &user)
|
||||||
|
assert.Equal(t, "user2", user.LoginName)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue