docs: preserve reviewed NetKingdom session artifacts
Some checks are pending
CI Smoke / host-smoke (push) Waiting to run
CI Smoke / container-smoke (push) Waiting to run
Reject generated-only commits / generated-commit-guard (push) Waiting to run

This commit is contained in:
codex 2026-11-01 23:23:46 +01:00
parent 4a15141c69
commit 4c8f8d8c26
49 changed files with 8701 additions and 0 deletions

View file

@ -0,0 +1,381 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package auth
import (
"context"
"fmt"
"reflect"
"forgejo.org/models/db"
"forgejo.org/modules/log"
"forgejo.org/modules/optional"
"forgejo.org/modules/timeutil"
"forgejo.org/modules/util"
"xorm.io/builder"
"xorm.io/xorm"
"xorm.io/xorm/convert"
)
// Type represents an login type.
type Type int
// Note: new type must append to the end of list to maintain compatibility.
const (
NoType Type = iota
Plain // 1
LDAP // 2
SMTP // 3
PAM // 4
DLDAP // 5
OAuth2 // 6
_ // 7 (was SSPI)
Remote // 8
)
// String returns the string name of the LoginType
func (typ Type) String() string {
return Names[typ]
}
// Int returns the int value of the LoginType
func (typ Type) Int() int {
return int(typ)
}
// Names contains the name of LoginType values.
var Names = map[Type]string{
LDAP: "LDAP (via BindDN)",
DLDAP: "LDAP (simple auth)", // Via direct bind
SMTP: "SMTP",
PAM: "PAM",
OAuth2: "OAuth2",
Remote: "Remote",
}
// Config represents login config as far as the db is concerned
type Config interface {
convert.Conversion
}
// SkipVerifiable configurations provide a IsSkipVerify to check if SkipVerify is set
type SkipVerifiable interface {
IsSkipVerify() bool
}
// HasTLSer configurations provide a HasTLS to check if TLS can be enabled
type HasTLSer interface {
HasTLS() bool
}
// UseTLSer configurations provide a HasTLS to check if TLS is enabled
type UseTLSer interface {
UseTLS() bool
}
// SSHKeyProvider configurations provide ProvidesSSHKeys to check if they provide SSHKeys
type SSHKeyProvider interface {
ProvidesSSHKeys() bool
}
// RegisterableSource configurations provide RegisterSource which needs to be run on creation
type RegisterableSource interface {
RegisterSource() error
UnregisterSource() error
}
var registeredConfigs = map[Type]func() Config{}
// RegisterTypeConfig register a config for a provided type
func RegisterTypeConfig(typ Type, exemplar Config) {
if reflect.TypeOf(exemplar).Kind() == reflect.Ptr {
// Pointer:
registeredConfigs[typ] = func() Config {
return reflect.New(reflect.ValueOf(exemplar).Elem().Type()).Interface().(Config)
}
return
}
// Not a Pointer
registeredConfigs[typ] = func() Config {
return reflect.New(reflect.TypeOf(exemplar)).Elem().Interface().(Config)
}
}
// SourceSettable configurations can have their authSource set on them
type SourceSettable interface {
SetAuthSource(*Source)
}
// Source represents an external way for authorizing users.
type Source struct {
ID int64 `xorm:"pk autoincr"`
Type Type
Name string `xorm:"UNIQUE"`
IsActive bool `xorm:"INDEX NOT NULL DEFAULT false"`
IsSyncEnabled bool `xorm:"INDEX NOT NULL DEFAULT false"`
Cfg convert.Conversion `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
}
// TableName xorm will read the table name from this method
func (Source) TableName() string {
return "login_source"
}
func init() {
db.RegisterModel(new(Source))
}
// BeforeSet is invoked from XORM before setting the value of a field of this object.
func (source *Source) BeforeSet(colName string, val xorm.Cell) {
if colName == "type" {
typ := Type(db.Cell2Int64(val))
constructor, ok := registeredConfigs[typ]
if !ok {
return
}
source.Cfg = constructor()
if settable, ok := source.Cfg.(SourceSettable); ok {
settable.SetAuthSource(source)
}
}
}
// TypeName return name of this login source type.
func (source *Source) TypeName() string {
return Names[source.Type]
}
// IsLDAP returns true of this source is of the LDAP type.
func (source *Source) IsLDAP() bool {
return source.Type == LDAP
}
// IsDLDAP returns true of this source is of the DLDAP type.
func (source *Source) IsDLDAP() bool {
return source.Type == DLDAP
}
// IsSMTP returns true of this source is of the SMTP type.
func (source *Source) IsSMTP() bool {
return source.Type == SMTP
}
// IsPAM returns true of this source is of the PAM type.
func (source *Source) IsPAM() bool {
return source.Type == PAM
}
// IsOAuth2 returns true of this source is of the OAuth2 type.
func (source *Source) IsOAuth2() bool {
return source.Type == OAuth2
}
func (source *Source) IsRemote() bool {
return source.Type == Remote
}
// HasTLS returns true of this source supports TLS.
func (source *Source) HasTLS() bool {
hasTLSer, ok := source.Cfg.(HasTLSer)
return ok && hasTLSer.HasTLS()
}
// UseTLS returns true of this source is configured to use TLS.
func (source *Source) UseTLS() bool {
useTLSer, ok := source.Cfg.(UseTLSer)
return ok && useTLSer.UseTLS()
}
// SkipVerify returns true if this source is configured to skip SSL
// verification.
func (source *Source) SkipVerify() bool {
skipVerifiable, ok := source.Cfg.(SkipVerifiable)
return ok && skipVerifiable.IsSkipVerify()
}
// CreateSource inserts a AuthSource in the DB if not already
// existing with the given name.
func CreateSource(ctx context.Context, source *Source) error {
has, err := db.GetEngine(ctx).Where("name=?", source.Name).Exist(new(Source))
if err != nil {
return err
} else if has {
return ErrSourceAlreadyExist{source.Name}
}
// Synchronization is only available with LDAP for now
if !source.IsLDAP() {
source.IsSyncEnabled = false
}
_, err = db.GetEngine(ctx).Insert(source)
if err != nil {
return err
}
if !source.IsActive {
return nil
}
if settable, ok := source.Cfg.(SourceSettable); ok {
settable.SetAuthSource(source)
}
registerableSource, ok := source.Cfg.(RegisterableSource)
if !ok {
return nil
}
err = registerableSource.RegisterSource()
if err != nil {
// remove the AuthSource in case of errors while registering configuration
if _, err := db.GetEngine(ctx).ID(source.ID).Delete(new(Source)); err != nil {
log.Error("CreateSource: Error while wrapOpenIDConnectInitializeError: %v", err)
}
}
return err
}
type FindSourcesOptions struct {
db.ListOptions
IsActive optional.Option[bool]
LoginType Type
}
func (opts FindSourcesOptions) ToConds() builder.Cond {
conds := builder.NewCond()
if opts.IsActive.Has() {
conds = conds.And(builder.Eq{"is_active": opts.IsActive.Value()})
}
if opts.LoginType != NoType {
conds = conds.And(builder.Eq{"`type`": opts.LoginType})
}
return conds
}
// GetSourceByID returns login source by given ID.
func GetSourceByID(ctx context.Context, id int64) (*Source, error) {
source := new(Source)
if id == 0 {
source.Cfg = registeredConfigs[NoType]()
// Set this source to active
// FIXME: allow disabling of db based password authentication in future
source.IsActive = true
return source, nil
}
has, err := db.GetEngine(ctx).ID(id).Get(source)
if err != nil {
return nil, err
} else if !has {
return nil, ErrSourceNotExist{id}
}
return source, nil
}
// UpdateSource updates a Source record in DB.
func UpdateSource(ctx context.Context, source *Source) error {
var originalSource *Source
if source.IsOAuth2() {
// keep track of the original values so we can restore in case of errors while registering OAuth2 providers
var err error
if originalSource, err = GetSourceByID(ctx, source.ID); err != nil {
return err
}
}
has, err := db.GetEngine(ctx).Where("name=? AND id!=?", source.Name, source.ID).Exist(new(Source))
if err != nil {
return err
} else if has {
return ErrSourceAlreadyExist{source.Name}
}
_, err = db.GetEngine(ctx).ID(source.ID).AllCols().Update(source)
if err != nil {
return err
}
if !source.IsActive {
return nil
}
if settable, ok := source.Cfg.(SourceSettable); ok {
settable.SetAuthSource(source)
}
registerableSource, ok := source.Cfg.(RegisterableSource)
if !ok {
return nil
}
err = registerableSource.RegisterSource()
if err != nil {
// restore original values since we cannot update the provider it self
if _, err := db.GetEngine(ctx).ID(source.ID).AllCols().Update(originalSource); err != nil {
log.Error("UpdateSource: Error while wrapOpenIDConnectInitializeError: %v", err)
}
}
return err
}
// ErrSourceNotExist represents a "SourceNotExist" kind of error.
type ErrSourceNotExist struct {
ID int64
}
// IsErrSourceNotExist checks if an error is a ErrSourceNotExist.
func IsErrSourceNotExist(err error) bool {
_, ok := err.(ErrSourceNotExist)
return ok
}
func (err ErrSourceNotExist) Error() string {
return fmt.Sprintf("login source does not exist [id: %d]", err.ID)
}
// Unwrap unwraps this as a ErrNotExist err
func (err ErrSourceNotExist) Unwrap() error {
return util.ErrNotExist
}
// ErrSourceAlreadyExist represents a "SourceAlreadyExist" kind of error.
type ErrSourceAlreadyExist struct {
Name string
}
// IsErrSourceAlreadyExist checks if an error is a ErrSourceAlreadyExist.
func IsErrSourceAlreadyExist(err error) bool {
_, ok := err.(ErrSourceAlreadyExist)
return ok
}
func (err ErrSourceAlreadyExist) Error() string {
return fmt.Sprintf("login source already exists [name: %s]", err.Name)
}
// Unwrap unwraps this as a ErrExist err
func (err ErrSourceAlreadyExist) Unwrap() error {
return util.ErrAlreadyExist
}
// ErrSourceInUse represents a "SourceInUse" kind of error.
type ErrSourceInUse struct {
ID int64
}
// IsErrSourceInUse checks if an error is a ErrSourceInUse.
func IsErrSourceInUse(err error) bool {
_, ok := err.(ErrSourceInUse)
return ok
}
func (err ErrSourceInUse) Error() string {
return fmt.Sprintf("login source is still used by some users [id: %d]", err.ID)
}

View file

@ -0,0 +1,42 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package auth
import (
"context"
"forgejo.org/models/auth"
"forgejo.org/models/db"
user_model "forgejo.org/models/user"
)
// DeleteSource deletes a AuthSource record in DB.
func DeleteSource(ctx context.Context, source *auth.Source) error {
count, err := db.GetEngine(ctx).Count(&user_model.User{LoginSource: source.ID})
if err != nil {
return err
} else if count > 0 {
return auth.ErrSourceInUse{
ID: source.ID,
}
}
count, err = db.GetEngine(ctx).Count(&user_model.ExternalLoginUser{LoginSourceID: source.ID})
if err != nil {
return err
} else if count > 0 {
return auth.ErrSourceInUse{
ID: source.ID,
}
}
if registerableSource, ok := source.Cfg.(auth.RegisterableSource); ok {
if err := registerableSource.UnregisterSource(); err != nil {
return err
}
}
_, err = db.GetEngine(ctx).ID(source.ID).Delete(new(auth.Source))
return err
}

View file

@ -0,0 +1,50 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package oauth2
import (
"fmt"
)
// RegisterSource causes an OAuth2 configuration to be registered
func (source *Source) RegisterSource() error {
err := RegisterProviderWithGothic(source.authSource.Name, source)
return wrapOpenIDConnectInitializeError(err, source.authSource.Name, source)
}
// UnregisterSource causes an OAuth2 configuration to be unregistered
func (source *Source) UnregisterSource() error {
RemoveProviderFromGothic(source.authSource.Name)
return nil
}
// ErrOpenIDConnectInitialize represents a "OpenIDConnectInitialize" kind of error.
type ErrOpenIDConnectInitialize struct {
OpenIDConnectAutoDiscoveryURL string
ProviderName string
Cause error
}
// IsErrOpenIDConnectInitialize checks if an error is a ExternalLoginUserAlreadyExist.
func IsErrOpenIDConnectInitialize(err error) bool {
_, ok := err.(ErrOpenIDConnectInitialize)
return ok
}
func (err ErrOpenIDConnectInitialize) Error() string {
return fmt.Sprintf("Failed to initialize OpenID Connect Provider with name '%s' with url '%s': %v", err.ProviderName, err.OpenIDConnectAutoDiscoveryURL, err.Cause)
}
func (err ErrOpenIDConnectInitialize) Unwrap() error {
return err.Cause
}
// wrapOpenIDConnectInitializeError is used to wrap the error but this cannot be done in modules/auth/oauth2
// inside oauth2: import cycle not allowed models -> modules/auth/oauth2 -> models
func wrapOpenIDConnectInitializeError(err error, providerName string, source *Source) error {
if err != nil && source.Provider == "openidConnect" {
err = ErrOpenIDConnectInitialize{ProviderName: providerName, OpenIDConnectAutoDiscoveryURL: source.OpenIDConnectAutoDiscoveryURL, Cause: err}
}
return err
}

View file

@ -0,0 +1,484 @@
--- a/cmd/admin_auth_oauth.go
+++ b/cmd/admin_auth_oauth.go
@@ -94,6 +94,10 @@
Name: "required-claim-value",
Value: "",
Usage: "Claim value that has to be set to allow users to login with this source",
+ },
+ &cli.StringSliceFlag{
+ Name: "required-claim-values",
+ Usage: "Require any one exact array value from current OpenID Connect UserInfo (cannot combine with required-claim-value)",
},
&cli.StringFlag{
Name: "group-claim-name",
@@ -160,6 +164,7 @@
Scopes: c.StringSlice("scopes"),
RequiredClaimName: c.String("required-claim-name"),
RequiredClaimValue: c.String("required-claim-value"),
+ RequiredClaimValues: c.StringSlice("required-claim-values"),
GroupClaimName: c.String("group-claim-name"),
AdminGroup: c.String("admin-group"),
RestrictedGroup: c.String("restricted-group"),
@@ -177,6 +182,12 @@
}
config := parseOAuth2Config(c)
+ if err := oauth2.ValidateRequiredClaimValues(config.RequiredClaimName, config.RequiredClaimValue, config.RequiredClaimValues); err != nil {
+ return err
+ }
+ if config.RequiredClaimValues != nil && config.Provider != "openidConnect" {
+ return errors.New("required-claim-values requires openidConnect")
+ }
if config.Provider == "openidConnect" {
discoveryURL, err := url.Parse(config.OpenIDConnectAutoDiscoveryURL)
if err != nil || (discoveryURL.Scheme != "http" && discoveryURL.Scheme != "https") {
@@ -246,6 +257,10 @@
oAuth2Config.RequiredClaimValue = c.String("required-claim-value")
}
+ if c.IsSet("required-claim-values") {
+ oAuth2Config.RequiredClaimValues = c.StringSlice("required-claim-values")
+ }
+
if c.IsSet("group-claim-name") {
oAuth2Config.GroupClaimName = c.String("group-claim-name")
}
@@ -293,6 +308,12 @@
}
oAuth2Config.CustomURLMapping = customURLMapping
+ if err := oauth2.ValidateRequiredClaimValues(oAuth2Config.RequiredClaimName, oAuth2Config.RequiredClaimValue, oAuth2Config.RequiredClaimValues); err != nil {
+ return err
+ }
+ if oAuth2Config.RequiredClaimValues != nil && oAuth2Config.Provider != "openidConnect" {
+ return errors.New("required-claim-values requires openidConnect")
+ }
source.Cfg = oAuth2Config
return auth_model.UpdateSource(ctx, source)
--- a/routers/web/admin/auths.go
+++ b/routers/web/admin/auths.go
@@ -322,6 +322,13 @@
ctx.Data["Source"] = source
ctx.Data["HasTLS"] = source.HasTLS()
+ // The legacy form has no representation for the opt-in current-UserInfo gate.
+ // Refuse rather than silently dropping it during source reconstruction.
+ if current, ok := source.Cfg.(*oauth2.Source); ok && current.RequiredClaimValues != nil {
+ ctx.RenderWithErr("This OAuth source uses required-claim-values; update it through the reviewed CLI.", tplAuthEdit, form)
+ return
+ }
+
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplAuthEdit)
return
--- a/routers/web/auth/oauth.go
+++ b/routers/web/auth/oauth.go
@@ -1427,7 +1427,11 @@
return goth.User{}, err
}
- if oauth2Source.RequiredClaimName != "" {
+ if oauth2Source.RequiredClaimValues != nil {
+ if oauth2Source.Provider != "openidConnect" || !oauth2.MatchRequiredClaimValues(gothUser.RawData, oauth2Source.RequiredClaimName, oauth2Source.RequiredClaimValue, oauth2Source.RequiredClaimValues) {
+ return goth.User{}, user_model.ErrUserProhibitLogin{Name: gothUser.UserID}
+ }
+ } else if oauth2Source.RequiredClaimName != "" {
claimInterface, has := gothUser.RawData[oauth2Source.RequiredClaimName]
if !has {
return goth.User{}, user_model.ErrUserProhibitLogin{Name: gothUser.UserID}
--- a/services/auth/source/oauth2/providers_openid.go
+++ b/services/auth/source/oauth2/providers_openid.go
@@ -4,6 +4,7 @@
package oauth2
import (
+ "errors"
"html/template"
"forgejo.org/modules/log"
@@ -43,6 +44,22 @@
if err != nil {
log.Warn("Failed to create OpenID Connect Provider with name '%s' with url '%s': %v", providerName, source.OpenIDConnectAutoDiscoveryURL, err)
}
+ if err != nil {
+ return nil, err
+ }
+ if source.RequiredClaimValues != nil {
+ if err := ValidateRequiredClaimValues(source.RequiredClaimName, source.RequiredClaimValue, source.RequiredClaimValues); err != nil {
+ return nil, err
+ }
+ if provider.OpenIDConfig == nil || provider.SkipUserInfoRequest {
+ return nil, errors.New("required-claim-values needs current UserInfo")
+ }
+ guarded, err := RequireCurrentUserInfoClaim(provider.Client(), provider.OpenIDConfig.UserInfoEndpoint, source.RequiredClaimName)
+ if err != nil {
+ return nil, err
+ }
+ provider.HTTPClient = guarded
+ }
return provider, err
}
--- /dev/null
+++ b/services/auth/source/oauth2/required_claim_values.go
@@ -0,0 +1,202 @@
+// Copyright 2026 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package oauth2
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+ "unicode"
+ "unicode/utf8"
+)
+
+const maxClaimValues = 1024
+const maxUserInfoBytes = 1 << 20
+
+// ValidateRequiredClaimValues validates only the opt-in list extension. A nil
+// list retains every legacy single-value behavior. An explicitly empty list
+// fails closed rather than turning a configured gate into claim-presence-only.
+func ValidateRequiredClaimValues(name, legacy string, values []string) error {
+ if values == nil {
+ return nil
+ }
+ if !validClaimLiteral(name) || legacy != "" || len(values) == 0 || len(values) > 64 {
+ return errors.New("invalid or conflicting required-claim-values configuration")
+ }
+ seen := map[string]bool{}
+ for _, value := range values {
+ if !validClaimLiteral(value) || seen[value] {
+ return errors.New("invalid required-claim-values literal")
+ }
+ seen[value] = true
+ }
+ return nil
+}
+
+func validClaimLiteral(value string) bool {
+ return value != "" && len(value) <= 256 && utf8.ValidString(value) &&
+ value == strings.TrimSpace(value) && !strings.ContainsAny(value, "\x00\r\n,") &&
+ !strings.ContainsFunc(value, func(r rune) bool { return unicode.IsControl(r) || r == utf8.RuneError })
+}
+
+// strictClaimValues accepts only a well-formed claim array. No scalar parsing,
+// coercion, regex or prefix matching is introduced into the new gate.
+func strictClaimValues(raw any) ([]string, bool) {
+ var values []string
+ switch v := raw.(type) {
+ case []string:
+ values = v
+ case []any:
+ values = make([]string, len(v))
+ for i, value := range v {
+ s, ok := value.(string)
+ if !ok {
+ return nil, false
+ }
+ values[i] = s
+ }
+ default:
+ return nil, false
+ }
+ if len(values) > maxClaimValues {
+ return nil, false
+ }
+ for _, value := range values {
+ if !validClaimLiteral(value) {
+ return nil, false
+ }
+ }
+ return values, true
+}
+
+// MatchRequiredClaimValues evaluates the new consumer gate against current
+// authenticated provider data; it does not mutate or manufacture issuer claims.
+func MatchRequiredClaimValues(claims map[string]any, name, legacy string, required []string) bool {
+ if required == nil || ValidateRequiredClaimValues(name, legacy, required) != nil {
+ return false
+ }
+ raw, present := claims[name]
+ if !present {
+ return false
+ }
+ values, ok := strictClaimValues(raw)
+ if !ok {
+ return false
+ }
+ for _, value := range values {
+ for _, wanted := range required {
+ if value == wanted {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+// RequireCurrentUserInfoClaim preserves the existing HTTP client settings and
+// adds a strict response guard for this opt-in source only. A current UserInfo
+// array must overwrite any stale ID-token claim. Missing/invalid data and HTTP
+// failures cannot fall back to the ID-token map in Goth's merging code.
+func RequireCurrentUserInfoClaim(client *http.Client, endpoint, claim string) (*http.Client, error) {
+ u, err := url.Parse(endpoint)
+ if err != nil || u.Scheme != "https" || u.Host == "" || u.User != nil || u.Fragment != "" || !validClaimLiteral(claim) {
+ return nil, errors.New("required-claim-values needs an HTTPS UserInfo endpoint")
+ }
+ if client == nil {
+ client = http.DefaultClient
+ }
+ result := *client
+ base := result.Transport
+ if base == nil {
+ base = http.DefaultTransport
+ }
+ result.Transport = &currentClaimTransport{base: base, endpoint: endpoint, claim: claim}
+ // Redirects could evade endpoint matching or send a bearer to another origin.
+ // They are unsupported for the new guarded provider; legacy clients are intact.
+ result.CheckRedirect = func(*http.Request, []*http.Request) error {
+ return errors.New("guarded OAuth redirects are not supported")
+ }
+ return &result, nil
+}
+
+type currentClaimTransport struct {
+ base http.RoundTripper
+ endpoint, claim string
+}
+
+func (t *currentClaimTransport) RoundTrip(req *http.Request) (*http.Response, error) {
+ response, err := t.base.RoundTrip(req)
+ if req.URL.String() != t.endpoint {
+ return response, err
+ }
+ if err != nil {
+ if response != nil && response.Body != nil {
+ response.Body.Close()
+ }
+ return nil, errors.New("current UserInfo lookup failed")
+ }
+ if response == nil || response.Body == nil {
+ return nil, errors.New("current UserInfo response missing")
+ }
+ defer response.Body.Close()
+ if req.Method != http.MethodGet || response.StatusCode != http.StatusOK {
+ return nil, errors.New("current UserInfo refused")
+ }
+ body, err := io.ReadAll(io.LimitReader(response.Body, maxUserInfoBytes+1))
+ if err != nil || len(body) > maxUserInfoBytes || !utf8.Valid(body) {
+ return nil, errors.New("current UserInfo response invalid")
+ }
+ // Reject duplicate top-level keys before Unmarshal can hide a conflicting
+ // groups or subject claim. json.RawMessage permits unrelated normal claims.
+ decoder := json.NewDecoder(bytes.NewReader(body))
+ token, err := decoder.Token()
+ if err != nil || token != json.Delim('{') {
+ return nil, errors.New("current UserInfo object required")
+ }
+ claims := map[string]json.RawMessage{}
+ for decoder.More() {
+ keyToken, err := decoder.Token()
+ if err != nil {
+ return nil, errors.New("current UserInfo key invalid")
+ }
+ key, ok := keyToken.(string)
+ if !ok {
+ return nil, errors.New("current UserInfo key invalid")
+ }
+ if _, duplicate := claims[key]; duplicate {
+ return nil, errors.New("current UserInfo duplicate claim")
+ }
+ var value json.RawMessage
+ if decoder.Decode(&value) != nil {
+ return nil, errors.New("current UserInfo value invalid")
+ }
+ claims[key] = value
+ }
+ if token, err = decoder.Token(); err != nil || token != json.Delim('}') {
+ return nil, errors.New("current UserInfo object invalid")
+ }
+ var trailing any
+ if decoder.Decode(&trailing) != io.EOF {
+ return nil, errors.New("current UserInfo trailing data")
+ }
+ raw, present := claims[t.claim]
+ if !present {
+ return nil, errors.New("current UserInfo required claim missing")
+ }
+ var value any
+ if json.Unmarshal(raw, &value) != nil {
+ return nil, errors.New("current UserInfo required claim invalid")
+ }
+ if _, ok := strictClaimValues(value); !ok {
+ return nil, errors.New("current UserInfo required claim array invalid")
+ }
+ // Return the original public response bytes to Goth. Subject equality and
+ // audience/issuer checks remain in its existing authenticated provider path.
+ response.Body = io.NopCloser(bytes.NewReader(body))
+ return response, nil
+}
--- /dev/null
+++ b/services/auth/source/oauth2/required_claim_values_test.go
@@ -0,0 +1,144 @@
+// Copyright 2026 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+package oauth2
+
+import (
+ "errors"
+ "io"
+ "net/http"
+ "reflect"
+ "strings"
+ "testing"
+)
+
+var caseValues = []string{"forge-admins", "forge-coulomb-owners", "forge-coulomb-readers"}
+
+type roundTripFunc func(*http.Request) (*http.Response, error)
+
+func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }
+
+func mockUserInfo(body string, status int) *http.Client {
+ return &http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
+ return &http.Response{StatusCode: status, Header: http.Header{}, Body: io.NopCloser(strings.NewReader(body)), Request: r}, nil
+ })}
+}
+
+func TestRequiredClaimExactAnyOfAndNoMutation(t *testing.T) {
+ for _, group := range caseValues {
+ groups := []any{group, "unrelated"}
+ claims := map[string]any{"groups": groups, "other": "preserved"}
+ if !MatchRequiredClaimValues(claims, "groups", "", caseValues) {
+ t.Fatal("individual allowed group rejected")
+ }
+ if !reflect.DeepEqual(claims, map[string]any{"groups": groups, "other": "preserved"}) {
+ t.Fatal("claims mutated")
+ }
+ }
+ if !MatchRequiredClaimValues(map[string]any{"groups": append([]string(nil), caseValues...)}, "groups", "", caseValues) {
+ t.Fatal("multiple allowed groups rejected")
+ }
+ for _, raw := range []any{nil, true, 1, "forge-admins", "forge-admins,forge-coulomb-owners", []any{"forge-admins", 1}, []any{"forge-admins", nil}, []string{}, []any{}, []string{"forge-admins-lookalike"}, []string{" Forge-admins"}, []string{"forge-admins\n"}, []string{"Forge-admins"}, []string{"forge-admins,other"}, []string{"forge-admins", ""}} {
+ if MatchRequiredClaimValues(map[string]any{"groups": raw}, "groups", "", caseValues) {
+ t.Fatal("malformed/nonmatching claim accepted")
+ }
+ }
+ if MatchRequiredClaimValues(map[string]any{}, "groups", "", caseValues) {
+ t.Fatal("absent claim accepted")
+ }
+ if MatchRequiredClaimValues(map[string]any{"unrelated": []string{"forge-admins"}}, "groups", "", caseValues) {
+ t.Fatal("unrelated claim used")
+ }
+}
+
+func TestRequiredClaimConfigurationRefusalsAndLegacyIsolation(t *testing.T) {
+ if ValidateRequiredClaimValues("", "old arbitrary literal", nil) != nil {
+ t.Fatal("legacy config changed")
+ }
+ for _, cfg := range []struct {
+ name, legacy string
+ values []string
+ }{
+ {"groups", "", []string{}}, {"groups", "", []string{""}}, {"groups", "old", caseValues}, {"", "", caseValues},
+ {"groups", "", []string{"a", "a"}}, {"groups", "", []string{"a,b"}}, {"groups", "", []string{" a"}},
+ } {
+ if ValidateRequiredClaimValues(cfg.name, cfg.legacy, cfg.values) == nil {
+ t.Fatal("invalid config accepted")
+ }
+ }
+ if ValidateRequiredClaimValues("groups", "", caseValues) != nil {
+ t.Fatal("case config invalid")
+ }
+}
+
+func TestCurrentUserInfoGuardRejectsMissingMalformedAndUnavailable(t *testing.T) {
+ for _, body := range []string{
+ `{}`, `{"sub":"alice"}`, `{"groups":null}`, `{"groups":true}`, `{"groups":"forge-admins"}`,
+ `{"groups":["forge-admins",null]}`, `{"groups":["forge-admins",1]}`,
+ `{"groups":["forge-admins"],"groups":[]}`, `{"groups":[]} {}`, `[]`, `{"groups":[]`,
+ `{"groups":["forge-admins",""]}`, strings.Repeat(" ", maxUserInfoBytes+1),
+ } {
+ client, err := RequireCurrentUserInfoClaim(mockUserInfo(body, 200), "https://issuer.example/userinfo", "groups")
+ if err != nil {
+ t.Fatal(err)
+ }
+ response, err := client.Get("https://issuer.example/userinfo")
+ if err == nil {
+ response.Body.Close()
+ t.Fatal("invalid current claim did not fail closed")
+ }
+ }
+ client, _ := RequireCurrentUserInfoClaim(mockUserInfo(`{"groups":[]}`, 503), "https://issuer.example/userinfo", "groups")
+ if response, err := client.Get("https://issuer.example/userinfo"); err == nil {
+ response.Body.Close()
+ t.Fatal("non200 accepted")
+ }
+ broken := &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) { return nil, errors.New("private backend error") })}
+ client, _ = RequireCurrentUserInfoClaim(broken, "https://issuer.example/userinfo", "groups")
+ if _, err := client.Get("https://issuer.example/userinfo"); err == nil || strings.Contains(err.Error(), "private backend") {
+ t.Fatal("lookup failure not contained")
+ }
+}
+
+func TestCurrentUserInfoEmptyArrayOverwritesAndGroupRemoval(t *testing.T) {
+ for _, body := range []string{`{"sub":"alice","groups":[]}`, `{"sub":"alice","groups":["forge-coulomb-readers"]}`} {
+ client, err := RequireCurrentUserInfoClaim(mockUserInfo(body, 200), "https://issuer.example/userinfo", "groups")
+ if err != nil {
+ t.Fatal(err)
+ }
+ response, err := client.Get("https://issuer.example/userinfo")
+ if err != nil {
+ t.Fatal(err)
+ }
+ b, _ := io.ReadAll(response.Body)
+ response.Body.Close()
+ if string(b) != body {
+ t.Fatal("response changed")
+ }
+ }
+}
+
+func TestCurrentUserInfoGuardIsOptInAndRedirectsRefuse(t *testing.T) {
+ original := mockUserInfo(`{"unrelated":true}`, 200)
+ guarded, err := RequireCurrentUserInfoClaim(original, "https://issuer.example/userinfo", "groups")
+ if err != nil {
+ t.Fatal(err)
+ }
+ response, err := original.Get("https://issuer.example/userinfo")
+ if err != nil {
+ t.Fatal("original client mutated")
+ }
+ response.Body.Close()
+ response, err = guarded.Get("https://issuer.example/unrelated")
+ if err != nil {
+ t.Fatal("unrelated endpoint changed")
+ }
+ response.Body.Close()
+ if guarded.CheckRedirect == nil || guarded.CheckRedirect(nil, nil) == nil {
+ t.Fatal("redirect can bypass guard")
+ }
+ for _, endpoint := range []string{"", "http://issuer.example/userinfo", "https://user:pass@issuer.example/userinfo", "https://issuer.example/userinfo#fragment"} {
+ if _, err := RequireCurrentUserInfoClaim(original, endpoint, "groups"); err == nil {
+ t.Fatal("invalid endpoint accepted")
+ }
+ }
+}
--- a/services/auth/source/oauth2/source.go
+++ b/services/auth/source/oauth2/source.go
@@ -22,6 +22,7 @@
Scopes []string
AttributeSSHPublicKey string
RequiredClaimName string
+ RequiredClaimValues []string `json:",omitempty"`
RequiredClaimValue string
GroupClaimName string
AdminGroup string

View file

@ -0,0 +1,320 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"errors"
"fmt"
"net/url"
auth_model "forgejo.org/models/auth"
"forgejo.org/services/auth/source/oauth2"
"github.com/urfave/cli/v2"
)
var (
oauthCLIFlags = []cli.Flag{
&cli.StringFlag{
Name: "name",
Value: "",
Usage: "Application Name",
},
&cli.StringFlag{
Name: "provider",
Value: "",
Usage: "OAuth2 Provider",
},
&cli.StringFlag{
Name: "key",
Value: "",
Usage: "Client ID (Key)",
},
&cli.StringFlag{
Name: "secret",
Value: "",
Usage: "Client Secret",
},
&cli.StringFlag{
Name: "auto-discover-url",
Value: "",
Usage: "OpenID Connect Auto Discovery URL (only required when using OpenID Connect as provider)",
},
&cli.StringFlag{
Name: "use-custom-urls",
Value: "false",
Usage: "Use custom URLs for GitLab/GitHub OAuth endpoints",
},
&cli.StringFlag{
Name: "custom-tenant-id",
Value: "",
Usage: "Use custom Tenant ID for OAuth endpoints",
},
&cli.StringFlag{
Name: "custom-auth-url",
Value: "",
Usage: "Use a custom Authorization URL (option for GitLab/GitHub)",
},
&cli.StringFlag{
Name: "custom-token-url",
Value: "",
Usage: "Use a custom Token URL (option for GitLab/GitHub)",
},
&cli.StringFlag{
Name: "custom-profile-url",
Value: "",
Usage: "Use a custom Profile URL (option for GitLab/GitHub)",
},
&cli.StringFlag{
Name: "custom-email-url",
Value: "",
Usage: "Use a custom Email URL (option for GitHub)",
},
&cli.StringFlag{
Name: "icon-url",
Value: "",
Usage: "Custom icon URL for OAuth2 login source",
},
&cli.BoolFlag{
Name: "skip-local-2fa",
Usage: "Set to true to skip local 2fa for users authenticated by this source",
},
&cli.StringSliceFlag{
Name: "scopes",
Value: nil,
Usage: "Scopes to request when to authenticate against this OAuth2 source",
},
&cli.StringFlag{
Name: "required-claim-name",
Value: "",
Usage: "Claim name that has to be set to allow users to login with this source",
},
&cli.StringFlag{
Name: "required-claim-value",
Value: "",
Usage: "Claim value that has to be set to allow users to login with this source",
},
&cli.StringSliceFlag{
Name: "required-claim-values",
Usage: "Require any one exact array value from current OpenID Connect UserInfo (cannot combine with required-claim-value)",
},
&cli.StringFlag{
Name: "group-claim-name",
Value: "",
Usage: "Claim name providing group names for this source",
},
&cli.StringFlag{
Name: "admin-group",
Value: "",
Usage: "Group Claim value for administrator users",
},
&cli.StringFlag{
Name: "restricted-group",
Value: "",
Usage: "Group Claim value for restricted users",
},
&cli.StringFlag{
Name: "group-team-map",
Value: "",
Usage: "JSON mapping between groups and org teams",
},
&cli.BoolFlag{
Name: "group-team-map-removal",
Usage: "Activate automatic team membership removal depending on groups",
},
}
microcmdAuthAddOauth = &cli.Command{
Name: "add-oauth",
Usage: "Add new Oauth authentication source",
Action: runAddOauth,
Flags: oauthCLIFlags,
}
microcmdAuthUpdateOauth = &cli.Command{
Name: "update-oauth",
Usage: "Update existing Oauth authentication source",
Action: runUpdateOauth,
Flags: append(oauthCLIFlags[:1], append([]cli.Flag{idFlag}, oauthCLIFlags[1:]...)...),
}
)
func parseOAuth2Config(c *cli.Context) *oauth2.Source {
var customURLMapping *oauth2.CustomURLMapping
if c.IsSet("use-custom-urls") {
customURLMapping = &oauth2.CustomURLMapping{
TokenURL: c.String("custom-token-url"),
AuthURL: c.String("custom-auth-url"),
ProfileURL: c.String("custom-profile-url"),
EmailURL: c.String("custom-email-url"),
Tenant: c.String("custom-tenant-id"),
}
} else {
customURLMapping = nil
}
return &oauth2.Source{
Provider: c.String("provider"),
ClientID: c.String("key"),
ClientSecret: c.String("secret"),
OpenIDConnectAutoDiscoveryURL: c.String("auto-discover-url"),
CustomURLMapping: customURLMapping,
IconURL: c.String("icon-url"),
SkipLocalTwoFA: c.Bool("skip-local-2fa"),
Scopes: c.StringSlice("scopes"),
RequiredClaimName: c.String("required-claim-name"),
RequiredClaimValue: c.String("required-claim-value"),
RequiredClaimValues: c.StringSlice("required-claim-values"),
GroupClaimName: c.String("group-claim-name"),
AdminGroup: c.String("admin-group"),
RestrictedGroup: c.String("restricted-group"),
GroupTeamMap: c.String("group-team-map"),
GroupTeamMapRemoval: c.Bool("group-team-map-removal"),
}
}
func runAddOauth(c *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()
if err := initDB(ctx); err != nil {
return err
}
config := parseOAuth2Config(c)
if err := oauth2.ValidateRequiredClaimValues(config.RequiredClaimName, config.RequiredClaimValue, config.RequiredClaimValues); err != nil {
return err
}
if config.RequiredClaimValues != nil && config.Provider != "openidConnect" {
return errors.New("required-claim-values requires openidConnect")
}
if config.Provider == "openidConnect" {
discoveryURL, err := url.Parse(config.OpenIDConnectAutoDiscoveryURL)
if err != nil || (discoveryURL.Scheme != "http" && discoveryURL.Scheme != "https") {
return fmt.Errorf("invalid Auto Discovery URL: %s (this must be a valid URL starting with http:// or https://)", config.OpenIDConnectAutoDiscoveryURL)
}
}
return auth_model.CreateSource(ctx, &auth_model.Source{
Type: auth_model.OAuth2,
Name: c.String("name"),
IsActive: true,
Cfg: config,
})
}
func runUpdateOauth(c *cli.Context) error {
if !c.IsSet("id") {
return errors.New("--id flag is missing")
}
ctx, cancel := installSignals()
defer cancel()
if err := initDB(ctx); err != nil {
return err
}
source, err := auth_model.GetSourceByID(ctx, c.Int64("id"))
if err != nil {
return err
}
oAuth2Config := source.Cfg.(*oauth2.Source)
if c.IsSet("name") {
source.Name = c.String("name")
}
if c.IsSet("provider") {
oAuth2Config.Provider = c.String("provider")
}
if c.IsSet("key") {
oAuth2Config.ClientID = c.String("key")
}
if c.IsSet("secret") {
oAuth2Config.ClientSecret = c.String("secret")
}
if c.IsSet("auto-discover-url") {
oAuth2Config.OpenIDConnectAutoDiscoveryURL = c.String("auto-discover-url")
}
if c.IsSet("icon-url") {
oAuth2Config.IconURL = c.String("icon-url")
}
if c.IsSet("scopes") {
oAuth2Config.Scopes = c.StringSlice("scopes")
}
if c.IsSet("required-claim-name") {
oAuth2Config.RequiredClaimName = c.String("required-claim-name")
}
if c.IsSet("required-claim-value") {
oAuth2Config.RequiredClaimValue = c.String("required-claim-value")
}
if c.IsSet("required-claim-values") {
oAuth2Config.RequiredClaimValues = c.StringSlice("required-claim-values")
}
if c.IsSet("group-claim-name") {
oAuth2Config.GroupClaimName = c.String("group-claim-name")
}
if c.IsSet("admin-group") {
oAuth2Config.AdminGroup = c.String("admin-group")
}
if c.IsSet("restricted-group") {
oAuth2Config.RestrictedGroup = c.String("restricted-group")
}
if c.IsSet("group-team-map") {
oAuth2Config.GroupTeamMap = c.String("group-team-map")
}
if c.IsSet("group-team-map-removal") {
oAuth2Config.GroupTeamMapRemoval = c.Bool("group-team-map-removal")
}
// update custom URL mapping
customURLMapping := &oauth2.CustomURLMapping{}
if oAuth2Config.CustomURLMapping != nil {
customURLMapping.TokenURL = oAuth2Config.CustomURLMapping.TokenURL
customURLMapping.AuthURL = oAuth2Config.CustomURLMapping.AuthURL
customURLMapping.ProfileURL = oAuth2Config.CustomURLMapping.ProfileURL
customURLMapping.EmailURL = oAuth2Config.CustomURLMapping.EmailURL
customURLMapping.Tenant = oAuth2Config.CustomURLMapping.Tenant
}
if c.IsSet("use-custom-urls") && c.IsSet("custom-token-url") {
customURLMapping.TokenURL = c.String("custom-token-url")
}
if c.IsSet("use-custom-urls") && c.IsSet("custom-auth-url") {
customURLMapping.AuthURL = c.String("custom-auth-url")
}
if c.IsSet("use-custom-urls") && c.IsSet("custom-profile-url") {
customURLMapping.ProfileURL = c.String("custom-profile-url")
}
if c.IsSet("use-custom-urls") && c.IsSet("custom-email-url") {
customURLMapping.EmailURL = c.String("custom-email-url")
}
if c.IsSet("use-custom-urls") && c.IsSet("custom-tenant-id") {
customURLMapping.Tenant = c.String("custom-tenant-id")
}
oAuth2Config.CustomURLMapping = customURLMapping
if err := oauth2.ValidateRequiredClaimValues(oAuth2Config.RequiredClaimName, oAuth2Config.RequiredClaimValue, oAuth2Config.RequiredClaimValues); err != nil {
return err
}
if oAuth2Config.RequiredClaimValues != nil && oAuth2Config.Provider != "openidConnect" {
return errors.New("required-claim-values requires openidConnect")
}
source.Cfg = oAuth2Config
return auth_model.UpdateSource(ctx, source)
}

View file

@ -0,0 +1,257 @@
module forgejo.org
go 1.24
toolchain go1.24.3
require (
code.forgejo.org/f3/gof3/v3 v3.10.6
code.forgejo.org/forgejo-contrib/go-libravatar v0.0.0-20191008002943-06d1c002b251
code.forgejo.org/forgejo/levelqueue v1.0.0
code.forgejo.org/forgejo/reply v1.0.2
code.forgejo.org/go-chi/binding v1.0.0
code.forgejo.org/go-chi/cache v1.0.0
code.forgejo.org/go-chi/captcha v1.0.1
code.forgejo.org/go-chi/session v1.0.1
code.gitea.io/actions-proto-go v0.4.0
code.gitea.io/sdk/gitea v0.20.0
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570
connectrpc.com/connect v1.17.0
github.com/42wim/httpsig v1.2.2
github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358
github.com/ProtonMail/go-crypto v1.1.6
github.com/PuerkitoBio/goquery v1.10.2
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.7.2
github.com/alecthomas/chroma/v2 v2.15.0
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
github.com/blevesearch/bleve/v2 v2.5.2
github.com/buildkite/terminal-to-html/v3 v3.16.8
github.com/caddyserver/certmagic v0.22.2
github.com/chi-middleware/proxy v1.1.1
github.com/djherbis/buffer v1.2.0
github.com/djherbis/nio/v3 v3.0.1
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707
github.com/dustin/go-humanize v1.0.1
github.com/editorconfig/editorconfig-core-go/v2 v2.6.3
github.com/emersion/go-imap v1.2.1
github.com/felixge/fgprof v0.9.5
github.com/fsnotify/fsnotify v1.8.0
github.com/gliderlabs/ssh v0.3.8
github.com/go-ap/activitypub v0.0.0-20231114162308-e219254dc5c9
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73
github.com/go-chi/chi/v5 v5.2.0
github.com/go-chi/cors v1.2.1
github.com/go-co-op/gocron v1.37.0
github.com/go-enry/go-enry/v2 v2.9.2
github.com/go-git/go-git/v5 v5.13.2
github.com/go-ldap/ldap/v3 v3.4.6
github.com/go-openapi/spec v0.20.14
github.com/go-sql-driver/mysql v1.9.1
github.com/go-webauthn/webauthn v0.12.2
github.com/gobwas/glob v0.2.3
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/google/go-github/v64 v64.0.0
github.com/google/pprof v0.0.0-20241017200806-017d972448fc
github.com/google/uuid v1.6.0
github.com/gorilla/feeds v1.2.0
github.com/gorilla/sessions v1.4.0
github.com/hashicorp/go-version v1.7.0
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/huandu/xstrings v1.5.0
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056
github.com/jhillyerd/enmime/v2 v2.1.0
github.com/json-iterator/go v1.1.12
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/klauspost/compress v1.17.11
github.com/klauspost/cpuid/v2 v2.2.10
github.com/lib/pq v1.10.9
github.com/markbates/goth v1.80.0
github.com/mattn/go-isatty v0.0.20
github.com/mattn/go-sqlite3 v1.14.28
github.com/meilisearch/meilisearch-go v0.31.0
github.com/mholt/archiver/v3 v3.5.1
github.com/microcosm-cc/bluemonday v1.0.27
github.com/minio/minio-go/v7 v7.0.88
github.com/msteinert/pam/v2 v2.1.0
github.com/nektos/act v0.2.52
github.com/niklasfasching/go-org v1.7.0
github.com/olivere/elastic/v7 v7.0.32
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.1
github.com/pquerna/otp v1.4.0
github.com/prometheus/client_golang v1.21.1
github.com/redis/go-redis/v9 v9.7.3
github.com/robfig/cron/v3 v3.0.1
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1
github.com/sassoftware/go-rpmutils v0.4.0
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3
github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92
github.com/stretchr/testify v1.10.0
github.com/syndtr/goleveldb v1.0.0
github.com/ulikunitz/xz v0.5.12
github.com/urfave/cli/v2 v2.27.6
github.com/valyala/fastjson v1.6.4
github.com/yohcop/openid-go v1.0.1
github.com/yuin/goldmark v1.7.8
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
gitlab.com/gitlab-org/api/client-go v0.126.0
go.uber.org/mock v0.5.0
golang.org/x/crypto v0.36.0
golang.org/x/image v0.25.0
golang.org/x/net v0.38.0
golang.org/x/oauth2 v0.28.0
golang.org/x/sync v0.12.0
golang.org/x/sys v0.31.0
golang.org/x/text v0.23.0
google.golang.org/protobuf v1.36.4
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/ini.v1 v1.67.0
gopkg.in/yaml.v3 v3.0.1
mvdan.cc/xurls/v2 v2.5.0
xorm.io/builder v0.3.13
xorm.io/xorm v1.3.9
)
require (
cloud.google.com/go/compute/metadata v0.6.0 // indirect
dario.cat/mergo v1.0.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect
github.com/DataDog/zstd v1.5.5 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/RoaringBitmap/roaring/v2 v2.4.5 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.22.0 // indirect
github.com/blevesearch/bleve_index_api v1.2.8 // indirect
github.com/blevesearch/geo v0.2.3 // indirect
github.com/blevesearch/go-faiss v1.0.25 // indirect
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
github.com/blevesearch/gtreap v0.1.1 // indirect
github.com/blevesearch/mmap-go v1.0.4 // indirect
github.com/blevesearch/scorch_segment_api/v2 v2.3.10 // indirect
github.com/blevesearch/segment v0.9.1 // indirect
github.com/blevesearch/snowballstem v0.9.0 // indirect
github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect
github.com/blevesearch/vellum v1.1.0 // indirect
github.com/blevesearch/zapx/v11 v11.4.2 // indirect
github.com/blevesearch/zapx/v12 v12.4.2 // indirect
github.com/blevesearch/zapx/v13 v13.4.2 // indirect
github.com/blevesearch/zapx/v14 v14.4.2 // indirect
github.com/blevesearch/zapx/v15 v15.4.2 // indirect
github.com/blevesearch/zapx/v16 v16.2.4 // indirect
github.com/boombuler/barcode v1.0.1 // indirect
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect
github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/cyphar/filepath-securejoin v0.3.6 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
github.com/go-ap/errors v0.0.0-20231003111023-183eef4b31b7 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
github.com/go-enry/go-oniguruma v1.2.1 // indirect
github.com/go-fed/httpsig v1.1.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-openapi/jsonpointer v0.20.2 // indirect
github.com/go-openapi/jsonreference v0.20.4 // indirect
github.com/go-openapi/swag v0.22.7 // indirect
github.com/go-webauthn/x v0.1.20 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/go-tpm v0.9.3 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/libdns/libdns v0.2.3 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/markbates/going v1.0.3 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mholt/acmez/v3 v3.1.1 // indirect
github.com/miekg/dns v1.1.63 // indirect
github.com/minio/crc64nvme v1.0.1 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 // indirect
github.com/mschoch/smat v0.2.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nwaples/rardecode v1.1.3 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/rhysd/actionlint v1.6.27 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/skeema/knownhosts v1.3.0 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
github.com/zeebo/assert v1.3.0 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
go.etcd.io/bbolt v1.4.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
go.uber.org/zap/exp v0.3.0 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/time v0.10.0 // indirect
golang.org/x/tools v0.31.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)
replace github.com/hashicorp/go-version => github.com/6543/go-version v1.3.1
replace github.com/shurcooL/vfsgen => github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0
replace github.com/nektos/act => code.forgejo.org/forgejo/act v1.25.1
replace github.com/mholt/archiver/v3 => code.forgejo.org/forgejo/archiver/v3 v3.5.1
replace github.com/gliderlabs/ssh => code.forgejo.org/forgejo/ssh v0.0.0-20241211213324-5fc306ca0616
replace git.sr.ht/~mariusor/go-xsd-duration => code.forgejo.org/forgejo/go-xsd-duration v0.0.0-20220703122237-02e73435a078

View file

@ -0,0 +1,408 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package admin
import (
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"forgejo.org/models/auth"
"forgejo.org/models/db"
"forgejo.org/modules/auth/pam"
"forgejo.org/modules/base"
"forgejo.org/modules/log"
"forgejo.org/modules/setting"
"forgejo.org/modules/web"
auth_service "forgejo.org/services/auth"
"forgejo.org/services/auth/source/ldap"
"forgejo.org/services/auth/source/oauth2"
pam_service "forgejo.org/services/auth/source/pam"
"forgejo.org/services/auth/source/smtp"
"forgejo.org/services/context"
"forgejo.org/services/forms"
"xorm.io/xorm/convert"
)
const (
tplAuths base.TplName = "admin/auth/list"
tplAuthNew base.TplName = "admin/auth/new"
tplAuthEdit base.TplName = "admin/auth/edit"
)
// Authentications show authentication config page
func Authentications(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.authentication")
ctx.Data["PageIsAdminAuthentications"] = true
var err error
ctx.Data["Sources"], ctx.Data["Total"], err = db.FindAndCount[auth.Source](ctx, auth.FindSourcesOptions{})
if err != nil {
ctx.ServerError("auth.Sources", err)
return
}
ctx.HTML(http.StatusOK, tplAuths)
}
type dropdownItem struct {
Name string
Type any
}
var (
authSources = func() []dropdownItem {
items := []dropdownItem{
{auth.LDAP.String(), auth.LDAP},
{auth.DLDAP.String(), auth.DLDAP},
{auth.SMTP.String(), auth.SMTP},
{auth.OAuth2.String(), auth.OAuth2},
}
if pam.Supported {
items = append(items, dropdownItem{auth.Names[auth.PAM], auth.PAM})
}
return items
}()
securityProtocols = []dropdownItem{
{ldap.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted], ldap.SecurityProtocolUnencrypted},
{ldap.SecurityProtocolNames[ldap.SecurityProtocolLDAPS], ldap.SecurityProtocolLDAPS},
{ldap.SecurityProtocolNames[ldap.SecurityProtocolStartTLS], ldap.SecurityProtocolStartTLS},
}
)
// NewAuthSource render adding a new auth source page
func NewAuthSource(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.auths.new")
ctx.Data["PageIsAdminAuthentications"] = true
ctx.Data["type"] = auth.LDAP.Int()
ctx.Data["CurrentTypeName"] = auth.Names[auth.LDAP]
ctx.Data["CurrentSecurityProtocol"] = ldap.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted]
ctx.Data["smtp_auth"] = "PLAIN"
ctx.Data["is_active"] = true
ctx.Data["is_sync_enabled"] = true
ctx.Data["AuthSources"] = authSources
ctx.Data["SecurityProtocols"] = securityProtocols
ctx.Data["SMTPAuths"] = smtp.Authenticators
oauth2providers := oauth2.GetSupportedOAuth2Providers()
ctx.Data["OAuth2Providers"] = oauth2providers
// only the first as default
ctx.Data["oauth2_provider"] = oauth2providers[0].Name()
ctx.HTML(http.StatusOK, tplAuthNew)
}
func parseLDAPConfig(form forms.AuthenticationForm) *ldap.Source {
var pageSize uint32
if form.UsePagedSearch {
pageSize = uint32(form.SearchPageSize)
}
return &ldap.Source{
Name: form.Name,
Host: form.Host,
Port: form.Port,
SecurityProtocol: ldap.SecurityProtocol(form.SecurityProtocol),
SkipVerify: form.SkipVerify,
BindDN: form.BindDN,
UserDN: form.UserDN,
BindPassword: form.BindPassword,
UserBase: form.UserBase,
DefaultDomainName: form.DefaultDomainName,
AttributeUsername: form.AttributeUsername,
AttributeName: form.AttributeName,
AttributeSurname: form.AttributeSurname,
AttributeMail: form.AttributeMail,
AttributesInBind: form.AttributesInBind,
AttributeSSHPublicKey: form.AttributeSSHPublicKey,
AttributeAvatar: form.AttributeAvatar,
SearchPageSize: pageSize,
Filter: form.Filter,
GroupsEnabled: form.GroupsEnabled,
GroupDN: form.GroupDN,
GroupFilter: form.GroupFilter,
GroupMemberUID: form.GroupMemberUID,
GroupTeamMap: form.GroupTeamMap,
GroupTeamMapRemoval: form.GroupTeamMapRemoval,
UserUID: form.UserUID,
AdminFilter: form.AdminFilter,
RestrictedFilter: form.RestrictedFilter,
AllowDeactivateAll: form.AllowDeactivateAll,
Enabled: true,
SkipLocalTwoFA: form.SkipLocalTwoFA,
}
}
func parseSMTPConfig(form forms.AuthenticationForm) *smtp.Source {
return &smtp.Source{
Auth: form.SMTPAuth,
Host: form.SMTPHost,
Port: form.SMTPPort,
AllowedDomains: form.AllowedDomains,
ForceSMTPS: form.ForceSMTPS,
SkipVerify: form.SkipVerify,
HeloHostname: form.HeloHostname,
DisableHelo: form.DisableHelo,
SkipLocalTwoFA: form.SkipLocalTwoFA,
}
}
func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source {
var customURLMapping *oauth2.CustomURLMapping
if form.Oauth2UseCustomURL {
customURLMapping = &oauth2.CustomURLMapping{
TokenURL: form.Oauth2TokenURL,
AuthURL: form.Oauth2AuthURL,
ProfileURL: form.Oauth2ProfileURL,
EmailURL: form.Oauth2EmailURL,
Tenant: form.Oauth2Tenant,
}
} else {
customURLMapping = nil
}
var scopes []string
for _, s := range strings.Split(form.Oauth2Scopes, ",") {
s = strings.TrimSpace(s)
if s != "" {
scopes = append(scopes, s)
}
}
return &oauth2.Source{
Provider: form.Oauth2Provider,
ClientID: form.Oauth2Key,
ClientSecret: form.Oauth2Secret,
OpenIDConnectAutoDiscoveryURL: form.OpenIDConnectAutoDiscoveryURL,
CustomURLMapping: customURLMapping,
IconURL: form.Oauth2IconURL,
Scopes: scopes,
AttributeSSHPublicKey: form.Oauth2AttributeSSHPublicKey,
RequiredClaimName: form.Oauth2RequiredClaimName,
RequiredClaimValue: form.Oauth2RequiredClaimValue,
SkipLocalTwoFA: form.SkipLocalTwoFA,
GroupClaimName: form.Oauth2GroupClaimName,
RestrictedGroup: form.Oauth2RestrictedGroup,
AdminGroup: form.Oauth2AdminGroup,
GroupTeamMap: form.Oauth2GroupTeamMap,
GroupTeamMapRemoval: form.Oauth2GroupTeamMapRemoval,
}
}
// NewAuthSourcePost response for adding an auth source
func NewAuthSourcePost(ctx *context.Context) {
form := *web.GetForm(ctx).(*forms.AuthenticationForm)
ctx.Data["Title"] = ctx.Tr("admin.auths.new")
ctx.Data["PageIsAdminAuthentications"] = true
ctx.Data["CurrentTypeName"] = auth.Type(form.Type).String()
ctx.Data["CurrentSecurityProtocol"] = ldap.SecurityProtocolNames[ldap.SecurityProtocol(form.SecurityProtocol)]
ctx.Data["AuthSources"] = authSources
ctx.Data["SecurityProtocols"] = securityProtocols
ctx.Data["SMTPAuths"] = smtp.Authenticators
oauth2providers := oauth2.GetSupportedOAuth2Providers()
ctx.Data["OAuth2Providers"] = oauth2providers
hasTLS := false
var config convert.Conversion
switch auth.Type(form.Type) {
case auth.LDAP, auth.DLDAP:
config = parseLDAPConfig(form)
hasTLS = ldap.SecurityProtocol(form.SecurityProtocol) > ldap.SecurityProtocolUnencrypted
case auth.SMTP:
config = parseSMTPConfig(form)
hasTLS = true
case auth.PAM:
config = &pam_service.Source{
ServiceName: form.PAMServiceName,
EmailDomain: form.PAMEmailDomain,
SkipLocalTwoFA: form.SkipLocalTwoFA,
}
case auth.OAuth2:
config = parseOAuth2Config(form)
oauth2Config := config.(*oauth2.Source)
if oauth2Config.Provider == "openidConnect" {
discoveryURL, err := url.Parse(oauth2Config.OpenIDConnectAutoDiscoveryURL)
if err != nil || (discoveryURL.Scheme != "http" && discoveryURL.Scheme != "https") {
ctx.Data["Err_DiscoveryURL"] = true
ctx.RenderWithErr(ctx.Tr("admin.auths.invalid_openIdConnectAutoDiscoveryURL"), tplAuthNew, form)
return
}
}
default:
ctx.Error(http.StatusBadRequest)
return
}
ctx.Data["HasTLS"] = hasTLS
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplAuthNew)
return
}
if err := auth.CreateSource(ctx, &auth.Source{
Type: auth.Type(form.Type),
Name: form.Name,
IsActive: form.IsActive,
IsSyncEnabled: form.IsSyncEnabled,
Cfg: config,
}); err != nil {
if auth.IsErrSourceAlreadyExist(err) {
ctx.Data["Err_Name"] = true
ctx.RenderWithErr(ctx.Tr("admin.auths.login_source_exist", err.(auth.ErrSourceAlreadyExist).Name), tplAuthNew, form)
} else if oauth2.IsErrOpenIDConnectInitialize(err) {
ctx.Data["Err_DiscoveryURL"] = true
unwrapped := err.(oauth2.ErrOpenIDConnectInitialize).Unwrap()
ctx.RenderWithErr(ctx.Tr("admin.auths.unable_to_initialize_openid", unwrapped), tplAuthNew, form)
} else {
ctx.ServerError("auth.CreateSource", err)
}
return
}
log.Trace("Authentication created by admin(%s): %s", ctx.Doer.Name, form.Name)
ctx.Flash.Success(ctx.Tr("admin.auths.new_success", form.Name))
ctx.Redirect(setting.AppSubURL + "/admin/auths")
}
// EditAuthSource render editing auth source page
func EditAuthSource(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.auths.edit")
ctx.Data["PageIsAdminAuthentications"] = true
ctx.Data["SecurityProtocols"] = securityProtocols
ctx.Data["SMTPAuths"] = smtp.Authenticators
oauth2providers := oauth2.GetSupportedOAuth2Providers()
ctx.Data["OAuth2Providers"] = oauth2providers
source, err := auth.GetSourceByID(ctx, ctx.ParamsInt64(":authid"))
if err != nil {
ctx.ServerError("auth.GetSourceByID", err)
return
}
ctx.Data["Source"] = source
ctx.Data["HasTLS"] = source.HasTLS()
if source.IsOAuth2() {
type Named interface {
Name() string
}
for _, provider := range oauth2providers {
if provider.Name() == source.Cfg.(Named).Name() {
ctx.Data["CurrentOAuth2Provider"] = provider
break
}
}
}
ctx.HTML(http.StatusOK, tplAuthEdit)
}
// EditAuthSourcePost response for editing auth source
func EditAuthSourcePost(ctx *context.Context) {
form := *web.GetForm(ctx).(*forms.AuthenticationForm)
ctx.Data["Title"] = ctx.Tr("admin.auths.edit")
ctx.Data["PageIsAdminAuthentications"] = true
ctx.Data["SMTPAuths"] = smtp.Authenticators
oauth2providers := oauth2.GetSupportedOAuth2Providers()
ctx.Data["OAuth2Providers"] = oauth2providers
source, err := auth.GetSourceByID(ctx, ctx.ParamsInt64(":authid"))
if err != nil {
ctx.ServerError("auth.GetSourceByID", err)
return
}
ctx.Data["Source"] = source
ctx.Data["HasTLS"] = source.HasTLS()
// The legacy form has no representation for the opt-in current-UserInfo gate.
// Refuse rather than silently dropping it during source reconstruction.
if current, ok := source.Cfg.(*oauth2.Source); ok && current.RequiredClaimValues != nil {
ctx.RenderWithErr("This OAuth source uses required-claim-values; update it through the reviewed CLI.", tplAuthEdit, form)
return
}
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplAuthEdit)
return
}
var config convert.Conversion
switch auth.Type(form.Type) {
case auth.LDAP, auth.DLDAP:
config = parseLDAPConfig(form)
case auth.SMTP:
config = parseSMTPConfig(form)
case auth.PAM:
config = &pam_service.Source{
ServiceName: form.PAMServiceName,
EmailDomain: form.PAMEmailDomain,
}
case auth.OAuth2:
config = parseOAuth2Config(form)
oauth2Config := config.(*oauth2.Source)
if oauth2Config.Provider == "openidConnect" {
discoveryURL, err := url.Parse(oauth2Config.OpenIDConnectAutoDiscoveryURL)
if err != nil || (discoveryURL.Scheme != "http" && discoveryURL.Scheme != "https") {
ctx.Data["Err_DiscoveryURL"] = true
ctx.RenderWithErr(ctx.Tr("admin.auths.invalid_openIdConnectAutoDiscoveryURL"), tplAuthEdit, form)
return
}
}
default:
ctx.Error(http.StatusBadRequest)
return
}
source.Name = form.Name
source.IsActive = form.IsActive
source.IsSyncEnabled = form.IsSyncEnabled
source.Cfg = config
if err := auth.UpdateSource(ctx, source); err != nil {
if auth.IsErrSourceAlreadyExist(err) {
ctx.Data["Err_Name"] = true
ctx.RenderWithErr(ctx.Tr("admin.auths.login_source_exist", err.(auth.ErrSourceAlreadyExist).Name), tplAuthEdit, form)
} else if oauth2.IsErrOpenIDConnectInitialize(err) {
ctx.Flash.Error(err.Error(), true)
ctx.Data["Err_DiscoveryURL"] = true
ctx.HTML(http.StatusOK, tplAuthEdit)
} else {
ctx.ServerError("UpdateSource", err)
}
return
}
log.Trace("Authentication changed by admin(%s): %d", ctx.Doer.Name, source.ID)
ctx.Flash.Success(ctx.Tr("admin.auths.update_success"))
ctx.Redirect(setting.AppSubURL + "/admin/auths/" + strconv.FormatInt(form.ID, 10))
}
// DeleteAuthSource response for deleting an auth source
func DeleteAuthSource(ctx *context.Context) {
source, err := auth.GetSourceByID(ctx, ctx.ParamsInt64(":authid"))
if err != nil {
ctx.ServerError("auth.GetSourceByID", err)
return
}
if err = auth_service.DeleteSource(ctx, source); err != nil {
if auth.IsErrSourceInUse(err) {
ctx.Flash.Error(ctx.Tr("admin.auths.still_in_used"))
} else {
ctx.Flash.Error(fmt.Sprintf("auth_service.DeleteSource: %v", err))
}
ctx.JSONRedirect(setting.AppSubURL + "/admin/auths/" + url.PathEscape(ctx.Params(":authid")))
return
}
log.Trace("Authentication deleted by admin(%s): %d", ctx.Doer.Name, source.ID)
ctx.Flash.Success(ctx.Tr("admin.auths.deletion_success"))
ctx.JSONRedirect(setting.AppSubURL + "/admin/auths")
}

View file

@ -0,0 +1,79 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package oauth2
import (
"errors"
"html/template"
"forgejo.org/modules/log"
"forgejo.org/modules/setting"
"forgejo.org/modules/svg"
"github.com/markbates/goth"
"github.com/markbates/goth/providers/openidConnect"
)
// OpenIDProvider is a GothProvider for OpenID
type OpenIDProvider struct{}
// Name provides the technical name for this provider
func (o *OpenIDProvider) Name() string {
return "openidConnect"
}
// DisplayName returns the friendly name for this provider
func (o *OpenIDProvider) DisplayName() string {
return "OpenID Connect"
}
// IconHTML returns icon HTML for this provider
func (o *OpenIDProvider) IconHTML(size int) template.HTML {
return svg.RenderHTML("gitea-openid", size, "tw-mr-2")
}
// CreateGothProvider creates a GothProvider from this Provider
func (o *OpenIDProvider) CreateGothProvider(providerName, callbackURL string, source *Source) (goth.Provider, error) {
scopes := setting.OAuth2Client.OpenIDConnectScopes
if len(scopes) == 0 {
scopes = append(scopes, source.Scopes...)
}
provider, err := openidConnect.New(source.ClientID, source.ClientSecret, callbackURL, source.OpenIDConnectAutoDiscoveryURL, scopes...)
if err != nil {
log.Warn("Failed to create OpenID Connect Provider with name '%s' with url '%s': %v", providerName, source.OpenIDConnectAutoDiscoveryURL, err)
}
if err != nil {
return nil, err
}
if source.RequiredClaimValues != nil {
if err := ValidateRequiredClaimValues(source.RequiredClaimName, source.RequiredClaimValue, source.RequiredClaimValues); err != nil {
return nil, err
}
if provider.OpenIDConfig == nil || provider.SkipUserInfoRequest {
return nil, errors.New("required-claim-values needs current UserInfo")
}
guarded, err := RequireCurrentUserInfoClaim(provider.Client(), provider.OpenIDConfig.UserInfoEndpoint, source.RequiredClaimName)
if err != nil {
return nil, err
}
provider.HTTPClient = guarded
}
return provider, err
}
// CustomURLSettings returns the custom url settings for this provider
func (o *OpenIDProvider) CustomURLSettings() *CustomURLSettings {
return nil
}
func (o *OpenIDProvider) CanProvideSSHKeys() bool {
return true
}
var _ GothProvider = &OpenIDProvider{}
func init() {
RegisterGothProvider(&OpenIDProvider{})
}

View file

@ -0,0 +1,202 @@
// Copyright 2026 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package oauth2
import (
"bytes"
"encoding/json"
"errors"
"io"
"net/http"
"net/url"
"strings"
"unicode"
"unicode/utf8"
)
const maxClaimValues = 1024
const maxUserInfoBytes = 1 << 20
// ValidateRequiredClaimValues validates only the opt-in list extension. A nil
// list retains every legacy single-value behavior. An explicitly empty list
// fails closed rather than turning a configured gate into claim-presence-only.
func ValidateRequiredClaimValues(name, legacy string, values []string) error {
if values == nil {
return nil
}
if !validClaimLiteral(name) || legacy != "" || len(values) == 0 || len(values) > 64 {
return errors.New("invalid or conflicting required-claim-values configuration")
}
seen := map[string]bool{}
for _, value := range values {
if !validClaimLiteral(value) || seen[value] {
return errors.New("invalid required-claim-values literal")
}
seen[value] = true
}
return nil
}
func validClaimLiteral(value string) bool {
return value != "" && len(value) <= 256 && utf8.ValidString(value) &&
value == strings.TrimSpace(value) && !strings.ContainsAny(value, "\x00\r\n,") &&
!strings.ContainsFunc(value, func(r rune) bool { return unicode.IsControl(r) || r == utf8.RuneError })
}
// strictClaimValues accepts only a well-formed claim array. No scalar parsing,
// coercion, regex or prefix matching is introduced into the new gate.
func strictClaimValues(raw any) ([]string, bool) {
var values []string
switch v := raw.(type) {
case []string:
values = v
case []any:
values = make([]string, len(v))
for i, value := range v {
s, ok := value.(string)
if !ok {
return nil, false
}
values[i] = s
}
default:
return nil, false
}
if len(values) > maxClaimValues {
return nil, false
}
for _, value := range values {
if !validClaimLiteral(value) {
return nil, false
}
}
return values, true
}
// MatchRequiredClaimValues evaluates the new consumer gate against current
// authenticated provider data; it does not mutate or manufacture issuer claims.
func MatchRequiredClaimValues(claims map[string]any, name, legacy string, required []string) bool {
if required == nil || ValidateRequiredClaimValues(name, legacy, required) != nil {
return false
}
raw, present := claims[name]
if !present {
return false
}
values, ok := strictClaimValues(raw)
if !ok {
return false
}
for _, value := range values {
for _, wanted := range required {
if value == wanted {
return true
}
}
}
return false
}
// RequireCurrentUserInfoClaim preserves the existing HTTP client settings and
// adds a strict response guard for this opt-in source only. A current UserInfo
// array must overwrite any stale ID-token claim. Missing/invalid data and HTTP
// failures cannot fall back to the ID-token map in Goth's merging code.
func RequireCurrentUserInfoClaim(client *http.Client, endpoint, claim string) (*http.Client, error) {
u, err := url.Parse(endpoint)
if err != nil || u.Scheme != "https" || u.Host == "" || u.User != nil || u.Fragment != "" || !validClaimLiteral(claim) {
return nil, errors.New("required-claim-values needs an HTTPS UserInfo endpoint")
}
if client == nil {
client = http.DefaultClient
}
result := *client
base := result.Transport
if base == nil {
base = http.DefaultTransport
}
result.Transport = &currentClaimTransport{base: base, endpoint: endpoint, claim: claim}
// Redirects could evade endpoint matching or send a bearer to another origin.
// They are unsupported for the new guarded provider; legacy clients are intact.
result.CheckRedirect = func(*http.Request, []*http.Request) error {
return errors.New("guarded OAuth redirects are not supported")
}
return &result, nil
}
type currentClaimTransport struct {
base http.RoundTripper
endpoint, claim string
}
func (t *currentClaimTransport) RoundTrip(req *http.Request) (*http.Response, error) {
response, err := t.base.RoundTrip(req)
if req.URL.String() != t.endpoint {
return response, err
}
if err != nil {
if response != nil && response.Body != nil {
response.Body.Close()
}
return nil, errors.New("current UserInfo lookup failed")
}
if response == nil || response.Body == nil {
return nil, errors.New("current UserInfo response missing")
}
defer response.Body.Close()
if req.Method != http.MethodGet || response.StatusCode != http.StatusOK {
return nil, errors.New("current UserInfo refused")
}
body, err := io.ReadAll(io.LimitReader(response.Body, maxUserInfoBytes+1))
if err != nil || len(body) > maxUserInfoBytes || !utf8.Valid(body) {
return nil, errors.New("current UserInfo response invalid")
}
// Reject duplicate top-level keys before Unmarshal can hide a conflicting
// groups or subject claim. json.RawMessage permits unrelated normal claims.
decoder := json.NewDecoder(bytes.NewReader(body))
token, err := decoder.Token()
if err != nil || token != json.Delim('{') {
return nil, errors.New("current UserInfo object required")
}
claims := map[string]json.RawMessage{}
for decoder.More() {
keyToken, err := decoder.Token()
if err != nil {
return nil, errors.New("current UserInfo key invalid")
}
key, ok := keyToken.(string)
if !ok {
return nil, errors.New("current UserInfo key invalid")
}
if _, duplicate := claims[key]; duplicate {
return nil, errors.New("current UserInfo duplicate claim")
}
var value json.RawMessage
if decoder.Decode(&value) != nil {
return nil, errors.New("current UserInfo value invalid")
}
claims[key] = value
}
if token, err = decoder.Token(); err != nil || token != json.Delim('}') {
return nil, errors.New("current UserInfo object invalid")
}
var trailing any
if decoder.Decode(&trailing) != io.EOF {
return nil, errors.New("current UserInfo trailing data")
}
raw, present := claims[t.claim]
if !present {
return nil, errors.New("current UserInfo required claim missing")
}
var value any
if json.Unmarshal(raw, &value) != nil {
return nil, errors.New("current UserInfo required claim invalid")
}
if _, ok := strictClaimValues(value); !ok {
return nil, errors.New("current UserInfo required claim array invalid")
}
// Return the original public response bytes to Goth. Subject equality and
// audience/issuer checks remain in its existing authenticated provider path.
response.Body = io.NopCloser(bytes.NewReader(body))
return response, nil
}

View file

@ -0,0 +1,144 @@
// Copyright 2026 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package oauth2
import (
"errors"
"io"
"net/http"
"reflect"
"strings"
"testing"
)
var caseValues = []string{"forge-admins", "forge-coulomb-owners", "forge-coulomb-readers"}
type roundTripFunc func(*http.Request) (*http.Response, error)
func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }
func mockUserInfo(body string, status int) *http.Client {
return &http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
return &http.Response{StatusCode: status, Header: http.Header{}, Body: io.NopCloser(strings.NewReader(body)), Request: r}, nil
})}
}
func TestRequiredClaimExactAnyOfAndNoMutation(t *testing.T) {
for _, group := range caseValues {
groups := []any{group, "unrelated"}
claims := map[string]any{"groups": groups, "other": "preserved"}
if !MatchRequiredClaimValues(claims, "groups", "", caseValues) {
t.Fatal("individual allowed group rejected")
}
if !reflect.DeepEqual(claims, map[string]any{"groups": groups, "other": "preserved"}) {
t.Fatal("claims mutated")
}
}
if !MatchRequiredClaimValues(map[string]any{"groups": append([]string(nil), caseValues...)}, "groups", "", caseValues) {
t.Fatal("multiple allowed groups rejected")
}
for _, raw := range []any{nil, true, 1, "forge-admins", "forge-admins,forge-coulomb-owners", []any{"forge-admins", 1}, []any{"forge-admins", nil}, []string{}, []any{}, []string{"forge-admins-lookalike"}, []string{" Forge-admins"}, []string{"forge-admins\n"}, []string{"Forge-admins"}, []string{"forge-admins,other"}, []string{"forge-admins", ""}} {
if MatchRequiredClaimValues(map[string]any{"groups": raw}, "groups", "", caseValues) {
t.Fatal("malformed/nonmatching claim accepted")
}
}
if MatchRequiredClaimValues(map[string]any{}, "groups", "", caseValues) {
t.Fatal("absent claim accepted")
}
if MatchRequiredClaimValues(map[string]any{"unrelated": []string{"forge-admins"}}, "groups", "", caseValues) {
t.Fatal("unrelated claim used")
}
}
func TestRequiredClaimConfigurationRefusalsAndLegacyIsolation(t *testing.T) {
if ValidateRequiredClaimValues("", "old arbitrary literal", nil) != nil {
t.Fatal("legacy config changed")
}
for _, cfg := range []struct {
name, legacy string
values []string
}{
{"groups", "", []string{}}, {"groups", "", []string{""}}, {"groups", "old", caseValues}, {"", "", caseValues},
{"groups", "", []string{"a", "a"}}, {"groups", "", []string{"a,b"}}, {"groups", "", []string{" a"}},
} {
if ValidateRequiredClaimValues(cfg.name, cfg.legacy, cfg.values) == nil {
t.Fatal("invalid config accepted")
}
}
if ValidateRequiredClaimValues("groups", "", caseValues) != nil {
t.Fatal("case config invalid")
}
}
func TestCurrentUserInfoGuardRejectsMissingMalformedAndUnavailable(t *testing.T) {
for _, body := range []string{
`{}`, `{"sub":"alice"}`, `{"groups":null}`, `{"groups":true}`, `{"groups":"forge-admins"}`,
`{"groups":["forge-admins",null]}`, `{"groups":["forge-admins",1]}`,
`{"groups":["forge-admins"],"groups":[]}`, `{"groups":[]} {}`, `[]`, `{"groups":[]`,
`{"groups":["forge-admins",""]}`, strings.Repeat(" ", maxUserInfoBytes+1),
} {
client, err := RequireCurrentUserInfoClaim(mockUserInfo(body, 200), "https://issuer.example/userinfo", "groups")
if err != nil {
t.Fatal(err)
}
response, err := client.Get("https://issuer.example/userinfo")
if err == nil {
response.Body.Close()
t.Fatal("invalid current claim did not fail closed")
}
}
client, _ := RequireCurrentUserInfoClaim(mockUserInfo(`{"groups":[]}`, 503), "https://issuer.example/userinfo", "groups")
if response, err := client.Get("https://issuer.example/userinfo"); err == nil {
response.Body.Close()
t.Fatal("non200 accepted")
}
broken := &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) { return nil, errors.New("private backend error") })}
client, _ = RequireCurrentUserInfoClaim(broken, "https://issuer.example/userinfo", "groups")
if _, err := client.Get("https://issuer.example/userinfo"); err == nil || strings.Contains(err.Error(), "private backend") {
t.Fatal("lookup failure not contained")
}
}
func TestCurrentUserInfoEmptyArrayOverwritesAndGroupRemoval(t *testing.T) {
for _, body := range []string{`{"sub":"alice","groups":[]}`, `{"sub":"alice","groups":["forge-coulomb-readers"]}`} {
client, err := RequireCurrentUserInfoClaim(mockUserInfo(body, 200), "https://issuer.example/userinfo", "groups")
if err != nil {
t.Fatal(err)
}
response, err := client.Get("https://issuer.example/userinfo")
if err != nil {
t.Fatal(err)
}
b, _ := io.ReadAll(response.Body)
response.Body.Close()
if string(b) != body {
t.Fatal("response changed")
}
}
}
func TestCurrentUserInfoGuardIsOptInAndRedirectsRefuse(t *testing.T) {
original := mockUserInfo(`{"unrelated":true}`, 200)
guarded, err := RequireCurrentUserInfoClaim(original, "https://issuer.example/userinfo", "groups")
if err != nil {
t.Fatal(err)
}
response, err := original.Get("https://issuer.example/userinfo")
if err != nil {
t.Fatal("original client mutated")
}
response.Body.Close()
response, err = guarded.Get("https://issuer.example/unrelated")
if err != nil {
t.Fatal("unrelated endpoint changed")
}
response.Body.Close()
if guarded.CheckRedirect == nil || guarded.CheckRedirect(nil, nil) == nil {
t.Fatal("redirect can bypass guard")
}
for _, endpoint := range []string{"", "http://issuer.example/userinfo", "https://user:pass@issuer.example/userinfo", "https://issuer.example/userinfo#fragment"} {
if _, err := RequireCurrentUserInfoClaim(original, endpoint, "groups"); err == nil {
t.Fatal("invalid endpoint accepted")
}
}
}

View file

@ -0,0 +1,60 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package oauth2
import (
"strings"
"forgejo.org/models/auth"
"forgejo.org/modules/json"
)
// Source holds configuration for the OAuth2 login source.
type Source struct {
Provider string
ClientID string
ClientSecret string
OpenIDConnectAutoDiscoveryURL string
CustomURLMapping *CustomURLMapping
IconURL string
Scopes []string
AttributeSSHPublicKey string
RequiredClaimName string
RequiredClaimValues []string `json:",omitempty"`
RequiredClaimValue string
GroupClaimName string
AdminGroup string
GroupTeamMap string
GroupTeamMapRemoval bool
RestrictedGroup string
SkipLocalTwoFA bool `json:",omitempty"`
// reference to the authSource
authSource *auth.Source
}
// FromDB fills up an OAuth2Config from serialized format.
func (source *Source) FromDB(bs []byte) error {
return json.UnmarshalHandleDoubleEncode(bs, &source)
}
// ToDB exports an SMTPConfig to a serialized format.
func (source *Source) ToDB() ([]byte, error) {
return json.Marshal(source)
}
// ProvidesSSHKeys returns if this source provides SSH Keys
func (source *Source) ProvidesSSHKeys() bool {
return len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0
}
// SetAuthSource sets the related AuthSource
func (source *Source) SetAuthSource(authSource *auth.Source) {
source.authSource = authSource
}
func init() {
auth.RegisterTypeConfig(auth.OAuth2, &Source{})
}

View file

@ -0,0 +1,88 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package forms
import (
"net/http"
"forgejo.org/modules/web/middleware"
"forgejo.org/services/context"
"code.forgejo.org/go-chi/binding"
)
// AuthenticationForm form for authentication
type AuthenticationForm struct {
ID int64
Type int `binding:"Range(2,7)"`
Name string `binding:"Required;MaxSize(30)"`
Host string
Port int
BindDN string
BindPassword string
UserBase string
UserDN string
AttributeUsername string
AttributeName string
AttributeSurname string
DefaultDomainName string
AttributeMail string
AttributeSSHPublicKey string
AttributeAvatar string
AttributesInBind bool
UsePagedSearch bool
SearchPageSize int
Filter string
AdminFilter string
GroupsEnabled bool
GroupDN string
GroupFilter string
GroupMemberUID string
UserUID string
RestrictedFilter string
AllowDeactivateAll bool
IsActive bool
IsSyncEnabled bool
SMTPAuth string
SMTPHost string
SMTPPort int
AllowedDomains string
SecurityProtocol int `binding:"Range(0,2)"`
TLS bool
SkipVerify bool
HeloHostname string
DisableHelo bool
ForceSMTPS bool
PAMServiceName string
PAMEmailDomain string
Oauth2Provider string
Oauth2Key string
Oauth2Secret string
OpenIDConnectAutoDiscoveryURL string
Oauth2UseCustomURL bool
Oauth2TokenURL string
Oauth2AuthURL string
Oauth2ProfileURL string
Oauth2EmailURL string
Oauth2IconURL string
Oauth2Tenant string
Oauth2Scopes string
Oauth2RequiredClaimName string
Oauth2RequiredClaimValue string
Oauth2GroupClaimName string
Oauth2AdminGroup string
Oauth2RestrictedGroup string
Oauth2GroupTeamMap string `binding:"ValidGroupTeamMap"`
Oauth2GroupTeamMapRemoval bool
Oauth2AttributeSSHPublicKey string
SkipLocalTwoFA bool
GroupTeamMap string `binding:"ValidGroupTeamMap"`
GroupTeamMapRemoval bool
}
// Validate validates fields
func (f *AuthenticationForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}

View file

@ -0,0 +1,299 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"errors"
"fmt"
"net/url"
auth_model "forgejo.org/models/auth"
"forgejo.org/services/auth/source/oauth2"
"github.com/urfave/cli/v2"
)
var (
oauthCLIFlags = []cli.Flag{
&cli.StringFlag{
Name: "name",
Value: "",
Usage: "Application Name",
},
&cli.StringFlag{
Name: "provider",
Value: "",
Usage: "OAuth2 Provider",
},
&cli.StringFlag{
Name: "key",
Value: "",
Usage: "Client ID (Key)",
},
&cli.StringFlag{
Name: "secret",
Value: "",
Usage: "Client Secret",
},
&cli.StringFlag{
Name: "auto-discover-url",
Value: "",
Usage: "OpenID Connect Auto Discovery URL (only required when using OpenID Connect as provider)",
},
&cli.StringFlag{
Name: "use-custom-urls",
Value: "false",
Usage: "Use custom URLs for GitLab/GitHub OAuth endpoints",
},
&cli.StringFlag{
Name: "custom-tenant-id",
Value: "",
Usage: "Use custom Tenant ID for OAuth endpoints",
},
&cli.StringFlag{
Name: "custom-auth-url",
Value: "",
Usage: "Use a custom Authorization URL (option for GitLab/GitHub)",
},
&cli.StringFlag{
Name: "custom-token-url",
Value: "",
Usage: "Use a custom Token URL (option for GitLab/GitHub)",
},
&cli.StringFlag{
Name: "custom-profile-url",
Value: "",
Usage: "Use a custom Profile URL (option for GitLab/GitHub)",
},
&cli.StringFlag{
Name: "custom-email-url",
Value: "",
Usage: "Use a custom Email URL (option for GitHub)",
},
&cli.StringFlag{
Name: "icon-url",
Value: "",
Usage: "Custom icon URL for OAuth2 login source",
},
&cli.BoolFlag{
Name: "skip-local-2fa",
Usage: "Set to true to skip local 2fa for users authenticated by this source",
},
&cli.StringSliceFlag{
Name: "scopes",
Value: nil,
Usage: "Scopes to request when to authenticate against this OAuth2 source",
},
&cli.StringFlag{
Name: "required-claim-name",
Value: "",
Usage: "Claim name that has to be set to allow users to login with this source",
},
&cli.StringFlag{
Name: "required-claim-value",
Value: "",
Usage: "Claim value that has to be set to allow users to login with this source",
},
&cli.StringFlag{
Name: "group-claim-name",
Value: "",
Usage: "Claim name providing group names for this source",
},
&cli.StringFlag{
Name: "admin-group",
Value: "",
Usage: "Group Claim value for administrator users",
},
&cli.StringFlag{
Name: "restricted-group",
Value: "",
Usage: "Group Claim value for restricted users",
},
&cli.StringFlag{
Name: "group-team-map",
Value: "",
Usage: "JSON mapping between groups and org teams",
},
&cli.BoolFlag{
Name: "group-team-map-removal",
Usage: "Activate automatic team membership removal depending on groups",
},
}
microcmdAuthAddOauth = &cli.Command{
Name: "add-oauth",
Usage: "Add new Oauth authentication source",
Action: runAddOauth,
Flags: oauthCLIFlags,
}
microcmdAuthUpdateOauth = &cli.Command{
Name: "update-oauth",
Usage: "Update existing Oauth authentication source",
Action: runUpdateOauth,
Flags: append(oauthCLIFlags[:1], append([]cli.Flag{idFlag}, oauthCLIFlags[1:]...)...),
}
)
func parseOAuth2Config(c *cli.Context) *oauth2.Source {
var customURLMapping *oauth2.CustomURLMapping
if c.IsSet("use-custom-urls") {
customURLMapping = &oauth2.CustomURLMapping{
TokenURL: c.String("custom-token-url"),
AuthURL: c.String("custom-auth-url"),
ProfileURL: c.String("custom-profile-url"),
EmailURL: c.String("custom-email-url"),
Tenant: c.String("custom-tenant-id"),
}
} else {
customURLMapping = nil
}
return &oauth2.Source{
Provider: c.String("provider"),
ClientID: c.String("key"),
ClientSecret: c.String("secret"),
OpenIDConnectAutoDiscoveryURL: c.String("auto-discover-url"),
CustomURLMapping: customURLMapping,
IconURL: c.String("icon-url"),
SkipLocalTwoFA: c.Bool("skip-local-2fa"),
Scopes: c.StringSlice("scopes"),
RequiredClaimName: c.String("required-claim-name"),
RequiredClaimValue: c.String("required-claim-value"),
GroupClaimName: c.String("group-claim-name"),
AdminGroup: c.String("admin-group"),
RestrictedGroup: c.String("restricted-group"),
GroupTeamMap: c.String("group-team-map"),
GroupTeamMapRemoval: c.Bool("group-team-map-removal"),
}
}
func runAddOauth(c *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()
if err := initDB(ctx); err != nil {
return err
}
config := parseOAuth2Config(c)
if config.Provider == "openidConnect" {
discoveryURL, err := url.Parse(config.OpenIDConnectAutoDiscoveryURL)
if err != nil || (discoveryURL.Scheme != "http" && discoveryURL.Scheme != "https") {
return fmt.Errorf("invalid Auto Discovery URL: %s (this must be a valid URL starting with http:// or https://)", config.OpenIDConnectAutoDiscoveryURL)
}
}
return auth_model.CreateSource(ctx, &auth_model.Source{
Type: auth_model.OAuth2,
Name: c.String("name"),
IsActive: true,
Cfg: config,
})
}
func runUpdateOauth(c *cli.Context) error {
if !c.IsSet("id") {
return errors.New("--id flag is missing")
}
ctx, cancel := installSignals()
defer cancel()
if err := initDB(ctx); err != nil {
return err
}
source, err := auth_model.GetSourceByID(ctx, c.Int64("id"))
if err != nil {
return err
}
oAuth2Config := source.Cfg.(*oauth2.Source)
if c.IsSet("name") {
source.Name = c.String("name")
}
if c.IsSet("provider") {
oAuth2Config.Provider = c.String("provider")
}
if c.IsSet("key") {
oAuth2Config.ClientID = c.String("key")
}
if c.IsSet("secret") {
oAuth2Config.ClientSecret = c.String("secret")
}
if c.IsSet("auto-discover-url") {
oAuth2Config.OpenIDConnectAutoDiscoveryURL = c.String("auto-discover-url")
}
if c.IsSet("icon-url") {
oAuth2Config.IconURL = c.String("icon-url")
}
if c.IsSet("scopes") {
oAuth2Config.Scopes = c.StringSlice("scopes")
}
if c.IsSet("required-claim-name") {
oAuth2Config.RequiredClaimName = c.String("required-claim-name")
}
if c.IsSet("required-claim-value") {
oAuth2Config.RequiredClaimValue = c.String("required-claim-value")
}
if c.IsSet("group-claim-name") {
oAuth2Config.GroupClaimName = c.String("group-claim-name")
}
if c.IsSet("admin-group") {
oAuth2Config.AdminGroup = c.String("admin-group")
}
if c.IsSet("restricted-group") {
oAuth2Config.RestrictedGroup = c.String("restricted-group")
}
if c.IsSet("group-team-map") {
oAuth2Config.GroupTeamMap = c.String("group-team-map")
}
if c.IsSet("group-team-map-removal") {
oAuth2Config.GroupTeamMapRemoval = c.Bool("group-team-map-removal")
}
// update custom URL mapping
customURLMapping := &oauth2.CustomURLMapping{}
if oAuth2Config.CustomURLMapping != nil {
customURLMapping.TokenURL = oAuth2Config.CustomURLMapping.TokenURL
customURLMapping.AuthURL = oAuth2Config.CustomURLMapping.AuthURL
customURLMapping.ProfileURL = oAuth2Config.CustomURLMapping.ProfileURL
customURLMapping.EmailURL = oAuth2Config.CustomURLMapping.EmailURL
customURLMapping.Tenant = oAuth2Config.CustomURLMapping.Tenant
}
if c.IsSet("use-custom-urls") && c.IsSet("custom-token-url") {
customURLMapping.TokenURL = c.String("custom-token-url")
}
if c.IsSet("use-custom-urls") && c.IsSet("custom-auth-url") {
customURLMapping.AuthURL = c.String("custom-auth-url")
}
if c.IsSet("use-custom-urls") && c.IsSet("custom-profile-url") {
customURLMapping.ProfileURL = c.String("custom-profile-url")
}
if c.IsSet("use-custom-urls") && c.IsSet("custom-email-url") {
customURLMapping.EmailURL = c.String("custom-email-url")
}
if c.IsSet("use-custom-urls") && c.IsSet("custom-tenant-id") {
customURLMapping.Tenant = c.String("custom-tenant-id")
}
oAuth2Config.CustomURLMapping = customURLMapping
source.Cfg = oAuth2Config
return auth_model.UpdateSource(ctx, source)
}

View file

@ -0,0 +1,257 @@
module forgejo.org
go 1.24
toolchain go1.24.3
require (
code.forgejo.org/f3/gof3/v3 v3.10.6
code.forgejo.org/forgejo-contrib/go-libravatar v0.0.0-20191008002943-06d1c002b251
code.forgejo.org/forgejo/levelqueue v1.0.0
code.forgejo.org/forgejo/reply v1.0.2
code.forgejo.org/go-chi/binding v1.0.0
code.forgejo.org/go-chi/cache v1.0.0
code.forgejo.org/go-chi/captcha v1.0.1
code.forgejo.org/go-chi/session v1.0.1
code.gitea.io/actions-proto-go v0.4.0
code.gitea.io/sdk/gitea v0.20.0
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570
connectrpc.com/connect v1.17.0
github.com/42wim/httpsig v1.2.2
github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358
github.com/ProtonMail/go-crypto v1.1.6
github.com/PuerkitoBio/goquery v1.10.2
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.7.2
github.com/alecthomas/chroma/v2 v2.15.0
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
github.com/blevesearch/bleve/v2 v2.5.2
github.com/buildkite/terminal-to-html/v3 v3.16.8
github.com/caddyserver/certmagic v0.22.2
github.com/chi-middleware/proxy v1.1.1
github.com/djherbis/buffer v1.2.0
github.com/djherbis/nio/v3 v3.0.1
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707
github.com/dustin/go-humanize v1.0.1
github.com/editorconfig/editorconfig-core-go/v2 v2.6.3
github.com/emersion/go-imap v1.2.1
github.com/felixge/fgprof v0.9.5
github.com/fsnotify/fsnotify v1.8.0
github.com/gliderlabs/ssh v0.3.8
github.com/go-ap/activitypub v0.0.0-20231114162308-e219254dc5c9
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73
github.com/go-chi/chi/v5 v5.2.0
github.com/go-chi/cors v1.2.1
github.com/go-co-op/gocron v1.37.0
github.com/go-enry/go-enry/v2 v2.9.2
github.com/go-git/go-git/v5 v5.13.2
github.com/go-ldap/ldap/v3 v3.4.6
github.com/go-openapi/spec v0.20.14
github.com/go-sql-driver/mysql v1.9.1
github.com/go-webauthn/webauthn v0.12.2
github.com/gobwas/glob v0.2.3
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/google/go-github/v64 v64.0.0
github.com/google/pprof v0.0.0-20241017200806-017d972448fc
github.com/google/uuid v1.6.0
github.com/gorilla/feeds v1.2.0
github.com/gorilla/sessions v1.4.0
github.com/hashicorp/go-version v1.7.0
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/huandu/xstrings v1.5.0
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056
github.com/jhillyerd/enmime/v2 v2.1.0
github.com/json-iterator/go v1.1.12
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/klauspost/compress v1.17.11
github.com/klauspost/cpuid/v2 v2.2.10
github.com/lib/pq v1.10.9
github.com/markbates/goth v1.80.0
github.com/mattn/go-isatty v0.0.20
github.com/mattn/go-sqlite3 v1.14.28
github.com/meilisearch/meilisearch-go v0.31.0
github.com/mholt/archiver/v3 v3.5.1
github.com/microcosm-cc/bluemonday v1.0.27
github.com/minio/minio-go/v7 v7.0.88
github.com/msteinert/pam/v2 v2.1.0
github.com/nektos/act v0.2.52
github.com/niklasfasching/go-org v1.7.0
github.com/olivere/elastic/v7 v7.0.32
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.1
github.com/pquerna/otp v1.4.0
github.com/prometheus/client_golang v1.21.1
github.com/redis/go-redis/v9 v9.7.3
github.com/robfig/cron/v3 v3.0.1
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1
github.com/sassoftware/go-rpmutils v0.4.0
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3
github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92
github.com/stretchr/testify v1.10.0
github.com/syndtr/goleveldb v1.0.0
github.com/ulikunitz/xz v0.5.12
github.com/urfave/cli/v2 v2.27.6
github.com/valyala/fastjson v1.6.4
github.com/yohcop/openid-go v1.0.1
github.com/yuin/goldmark v1.7.8
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
gitlab.com/gitlab-org/api/client-go v0.126.0
go.uber.org/mock v0.5.0
golang.org/x/crypto v0.36.0
golang.org/x/image v0.25.0
golang.org/x/net v0.38.0
golang.org/x/oauth2 v0.28.0
golang.org/x/sync v0.12.0
golang.org/x/sys v0.31.0
golang.org/x/text v0.23.0
google.golang.org/protobuf v1.36.4
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/ini.v1 v1.67.0
gopkg.in/yaml.v3 v3.0.1
mvdan.cc/xurls/v2 v2.5.0
xorm.io/builder v0.3.13
xorm.io/xorm v1.3.9
)
require (
cloud.google.com/go/compute/metadata v0.6.0 // indirect
dario.cat/mergo v1.0.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect
github.com/DataDog/zstd v1.5.5 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/RoaringBitmap/roaring/v2 v2.4.5 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.22.0 // indirect
github.com/blevesearch/bleve_index_api v1.2.8 // indirect
github.com/blevesearch/geo v0.2.3 // indirect
github.com/blevesearch/go-faiss v1.0.25 // indirect
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
github.com/blevesearch/gtreap v0.1.1 // indirect
github.com/blevesearch/mmap-go v1.0.4 // indirect
github.com/blevesearch/scorch_segment_api/v2 v2.3.10 // indirect
github.com/blevesearch/segment v0.9.1 // indirect
github.com/blevesearch/snowballstem v0.9.0 // indirect
github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect
github.com/blevesearch/vellum v1.1.0 // indirect
github.com/blevesearch/zapx/v11 v11.4.2 // indirect
github.com/blevesearch/zapx/v12 v12.4.2 // indirect
github.com/blevesearch/zapx/v13 v13.4.2 // indirect
github.com/blevesearch/zapx/v14 v14.4.2 // indirect
github.com/blevesearch/zapx/v15 v15.4.2 // indirect
github.com/blevesearch/zapx/v16 v16.2.4 // indirect
github.com/boombuler/barcode v1.0.1 // indirect
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect
github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/cyphar/filepath-securejoin v0.3.6 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
github.com/go-ap/errors v0.0.0-20231003111023-183eef4b31b7 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
github.com/go-enry/go-oniguruma v1.2.1 // indirect
github.com/go-fed/httpsig v1.1.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-openapi/jsonpointer v0.20.2 // indirect
github.com/go-openapi/jsonreference v0.20.4 // indirect
github.com/go-openapi/swag v0.22.7 // indirect
github.com/go-webauthn/x v0.1.20 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/go-tpm v0.9.3 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/libdns/libdns v0.2.3 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/markbates/going v1.0.3 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mholt/acmez/v3 v3.1.1 // indirect
github.com/miekg/dns v1.1.63 // indirect
github.com/minio/crc64nvme v1.0.1 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 // indirect
github.com/mschoch/smat v0.2.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nwaples/rardecode v1.1.3 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/rhysd/actionlint v1.6.27 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/skeema/knownhosts v1.3.0 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
github.com/zeebo/assert v1.3.0 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
go.etcd.io/bbolt v1.4.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
go.uber.org/zap/exp v0.3.0 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/time v0.10.0 // indirect
golang.org/x/tools v0.31.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)
replace github.com/hashicorp/go-version => github.com/6543/go-version v1.3.1
replace github.com/shurcooL/vfsgen => github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0
replace github.com/nektos/act => code.forgejo.org/forgejo/act v1.25.1
replace github.com/mholt/archiver/v3 => code.forgejo.org/forgejo/archiver/v3 v3.5.1
replace github.com/gliderlabs/ssh => code.forgejo.org/forgejo/ssh v0.0.0-20241211213324-5fc306ca0616
replace git.sr.ht/~mariusor/go-xsd-duration => code.forgejo.org/forgejo/go-xsd-duration v0.0.0-20220703122237-02e73435a078

View file

@ -0,0 +1,401 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package admin
import (
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"forgejo.org/models/auth"
"forgejo.org/models/db"
"forgejo.org/modules/auth/pam"
"forgejo.org/modules/base"
"forgejo.org/modules/log"
"forgejo.org/modules/setting"
"forgejo.org/modules/web"
auth_service "forgejo.org/services/auth"
"forgejo.org/services/auth/source/ldap"
"forgejo.org/services/auth/source/oauth2"
pam_service "forgejo.org/services/auth/source/pam"
"forgejo.org/services/auth/source/smtp"
"forgejo.org/services/context"
"forgejo.org/services/forms"
"xorm.io/xorm/convert"
)
const (
tplAuths base.TplName = "admin/auth/list"
tplAuthNew base.TplName = "admin/auth/new"
tplAuthEdit base.TplName = "admin/auth/edit"
)
// Authentications show authentication config page
func Authentications(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.authentication")
ctx.Data["PageIsAdminAuthentications"] = true
var err error
ctx.Data["Sources"], ctx.Data["Total"], err = db.FindAndCount[auth.Source](ctx, auth.FindSourcesOptions{})
if err != nil {
ctx.ServerError("auth.Sources", err)
return
}
ctx.HTML(http.StatusOK, tplAuths)
}
type dropdownItem struct {
Name string
Type any
}
var (
authSources = func() []dropdownItem {
items := []dropdownItem{
{auth.LDAP.String(), auth.LDAP},
{auth.DLDAP.String(), auth.DLDAP},
{auth.SMTP.String(), auth.SMTP},
{auth.OAuth2.String(), auth.OAuth2},
}
if pam.Supported {
items = append(items, dropdownItem{auth.Names[auth.PAM], auth.PAM})
}
return items
}()
securityProtocols = []dropdownItem{
{ldap.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted], ldap.SecurityProtocolUnencrypted},
{ldap.SecurityProtocolNames[ldap.SecurityProtocolLDAPS], ldap.SecurityProtocolLDAPS},
{ldap.SecurityProtocolNames[ldap.SecurityProtocolStartTLS], ldap.SecurityProtocolStartTLS},
}
)
// NewAuthSource render adding a new auth source page
func NewAuthSource(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.auths.new")
ctx.Data["PageIsAdminAuthentications"] = true
ctx.Data["type"] = auth.LDAP.Int()
ctx.Data["CurrentTypeName"] = auth.Names[auth.LDAP]
ctx.Data["CurrentSecurityProtocol"] = ldap.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted]
ctx.Data["smtp_auth"] = "PLAIN"
ctx.Data["is_active"] = true
ctx.Data["is_sync_enabled"] = true
ctx.Data["AuthSources"] = authSources
ctx.Data["SecurityProtocols"] = securityProtocols
ctx.Data["SMTPAuths"] = smtp.Authenticators
oauth2providers := oauth2.GetSupportedOAuth2Providers()
ctx.Data["OAuth2Providers"] = oauth2providers
// only the first as default
ctx.Data["oauth2_provider"] = oauth2providers[0].Name()
ctx.HTML(http.StatusOK, tplAuthNew)
}
func parseLDAPConfig(form forms.AuthenticationForm) *ldap.Source {
var pageSize uint32
if form.UsePagedSearch {
pageSize = uint32(form.SearchPageSize)
}
return &ldap.Source{
Name: form.Name,
Host: form.Host,
Port: form.Port,
SecurityProtocol: ldap.SecurityProtocol(form.SecurityProtocol),
SkipVerify: form.SkipVerify,
BindDN: form.BindDN,
UserDN: form.UserDN,
BindPassword: form.BindPassword,
UserBase: form.UserBase,
DefaultDomainName: form.DefaultDomainName,
AttributeUsername: form.AttributeUsername,
AttributeName: form.AttributeName,
AttributeSurname: form.AttributeSurname,
AttributeMail: form.AttributeMail,
AttributesInBind: form.AttributesInBind,
AttributeSSHPublicKey: form.AttributeSSHPublicKey,
AttributeAvatar: form.AttributeAvatar,
SearchPageSize: pageSize,
Filter: form.Filter,
GroupsEnabled: form.GroupsEnabled,
GroupDN: form.GroupDN,
GroupFilter: form.GroupFilter,
GroupMemberUID: form.GroupMemberUID,
GroupTeamMap: form.GroupTeamMap,
GroupTeamMapRemoval: form.GroupTeamMapRemoval,
UserUID: form.UserUID,
AdminFilter: form.AdminFilter,
RestrictedFilter: form.RestrictedFilter,
AllowDeactivateAll: form.AllowDeactivateAll,
Enabled: true,
SkipLocalTwoFA: form.SkipLocalTwoFA,
}
}
func parseSMTPConfig(form forms.AuthenticationForm) *smtp.Source {
return &smtp.Source{
Auth: form.SMTPAuth,
Host: form.SMTPHost,
Port: form.SMTPPort,
AllowedDomains: form.AllowedDomains,
ForceSMTPS: form.ForceSMTPS,
SkipVerify: form.SkipVerify,
HeloHostname: form.HeloHostname,
DisableHelo: form.DisableHelo,
SkipLocalTwoFA: form.SkipLocalTwoFA,
}
}
func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source {
var customURLMapping *oauth2.CustomURLMapping
if form.Oauth2UseCustomURL {
customURLMapping = &oauth2.CustomURLMapping{
TokenURL: form.Oauth2TokenURL,
AuthURL: form.Oauth2AuthURL,
ProfileURL: form.Oauth2ProfileURL,
EmailURL: form.Oauth2EmailURL,
Tenant: form.Oauth2Tenant,
}
} else {
customURLMapping = nil
}
var scopes []string
for _, s := range strings.Split(form.Oauth2Scopes, ",") {
s = strings.TrimSpace(s)
if s != "" {
scopes = append(scopes, s)
}
}
return &oauth2.Source{
Provider: form.Oauth2Provider,
ClientID: form.Oauth2Key,
ClientSecret: form.Oauth2Secret,
OpenIDConnectAutoDiscoveryURL: form.OpenIDConnectAutoDiscoveryURL,
CustomURLMapping: customURLMapping,
IconURL: form.Oauth2IconURL,
Scopes: scopes,
AttributeSSHPublicKey: form.Oauth2AttributeSSHPublicKey,
RequiredClaimName: form.Oauth2RequiredClaimName,
RequiredClaimValue: form.Oauth2RequiredClaimValue,
SkipLocalTwoFA: form.SkipLocalTwoFA,
GroupClaimName: form.Oauth2GroupClaimName,
RestrictedGroup: form.Oauth2RestrictedGroup,
AdminGroup: form.Oauth2AdminGroup,
GroupTeamMap: form.Oauth2GroupTeamMap,
GroupTeamMapRemoval: form.Oauth2GroupTeamMapRemoval,
}
}
// NewAuthSourcePost response for adding an auth source
func NewAuthSourcePost(ctx *context.Context) {
form := *web.GetForm(ctx).(*forms.AuthenticationForm)
ctx.Data["Title"] = ctx.Tr("admin.auths.new")
ctx.Data["PageIsAdminAuthentications"] = true
ctx.Data["CurrentTypeName"] = auth.Type(form.Type).String()
ctx.Data["CurrentSecurityProtocol"] = ldap.SecurityProtocolNames[ldap.SecurityProtocol(form.SecurityProtocol)]
ctx.Data["AuthSources"] = authSources
ctx.Data["SecurityProtocols"] = securityProtocols
ctx.Data["SMTPAuths"] = smtp.Authenticators
oauth2providers := oauth2.GetSupportedOAuth2Providers()
ctx.Data["OAuth2Providers"] = oauth2providers
hasTLS := false
var config convert.Conversion
switch auth.Type(form.Type) {
case auth.LDAP, auth.DLDAP:
config = parseLDAPConfig(form)
hasTLS = ldap.SecurityProtocol(form.SecurityProtocol) > ldap.SecurityProtocolUnencrypted
case auth.SMTP:
config = parseSMTPConfig(form)
hasTLS = true
case auth.PAM:
config = &pam_service.Source{
ServiceName: form.PAMServiceName,
EmailDomain: form.PAMEmailDomain,
SkipLocalTwoFA: form.SkipLocalTwoFA,
}
case auth.OAuth2:
config = parseOAuth2Config(form)
oauth2Config := config.(*oauth2.Source)
if oauth2Config.Provider == "openidConnect" {
discoveryURL, err := url.Parse(oauth2Config.OpenIDConnectAutoDiscoveryURL)
if err != nil || (discoveryURL.Scheme != "http" && discoveryURL.Scheme != "https") {
ctx.Data["Err_DiscoveryURL"] = true
ctx.RenderWithErr(ctx.Tr("admin.auths.invalid_openIdConnectAutoDiscoveryURL"), tplAuthNew, form)
return
}
}
default:
ctx.Error(http.StatusBadRequest)
return
}
ctx.Data["HasTLS"] = hasTLS
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplAuthNew)
return
}
if err := auth.CreateSource(ctx, &auth.Source{
Type: auth.Type(form.Type),
Name: form.Name,
IsActive: form.IsActive,
IsSyncEnabled: form.IsSyncEnabled,
Cfg: config,
}); err != nil {
if auth.IsErrSourceAlreadyExist(err) {
ctx.Data["Err_Name"] = true
ctx.RenderWithErr(ctx.Tr("admin.auths.login_source_exist", err.(auth.ErrSourceAlreadyExist).Name), tplAuthNew, form)
} else if oauth2.IsErrOpenIDConnectInitialize(err) {
ctx.Data["Err_DiscoveryURL"] = true
unwrapped := err.(oauth2.ErrOpenIDConnectInitialize).Unwrap()
ctx.RenderWithErr(ctx.Tr("admin.auths.unable_to_initialize_openid", unwrapped), tplAuthNew, form)
} else {
ctx.ServerError("auth.CreateSource", err)
}
return
}
log.Trace("Authentication created by admin(%s): %s", ctx.Doer.Name, form.Name)
ctx.Flash.Success(ctx.Tr("admin.auths.new_success", form.Name))
ctx.Redirect(setting.AppSubURL + "/admin/auths")
}
// EditAuthSource render editing auth source page
func EditAuthSource(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.auths.edit")
ctx.Data["PageIsAdminAuthentications"] = true
ctx.Data["SecurityProtocols"] = securityProtocols
ctx.Data["SMTPAuths"] = smtp.Authenticators
oauth2providers := oauth2.GetSupportedOAuth2Providers()
ctx.Data["OAuth2Providers"] = oauth2providers
source, err := auth.GetSourceByID(ctx, ctx.ParamsInt64(":authid"))
if err != nil {
ctx.ServerError("auth.GetSourceByID", err)
return
}
ctx.Data["Source"] = source
ctx.Data["HasTLS"] = source.HasTLS()
if source.IsOAuth2() {
type Named interface {
Name() string
}
for _, provider := range oauth2providers {
if provider.Name() == source.Cfg.(Named).Name() {
ctx.Data["CurrentOAuth2Provider"] = provider
break
}
}
}
ctx.HTML(http.StatusOK, tplAuthEdit)
}
// EditAuthSourcePost response for editing auth source
func EditAuthSourcePost(ctx *context.Context) {
form := *web.GetForm(ctx).(*forms.AuthenticationForm)
ctx.Data["Title"] = ctx.Tr("admin.auths.edit")
ctx.Data["PageIsAdminAuthentications"] = true
ctx.Data["SMTPAuths"] = smtp.Authenticators
oauth2providers := oauth2.GetSupportedOAuth2Providers()
ctx.Data["OAuth2Providers"] = oauth2providers
source, err := auth.GetSourceByID(ctx, ctx.ParamsInt64(":authid"))
if err != nil {
ctx.ServerError("auth.GetSourceByID", err)
return
}
ctx.Data["Source"] = source
ctx.Data["HasTLS"] = source.HasTLS()
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplAuthEdit)
return
}
var config convert.Conversion
switch auth.Type(form.Type) {
case auth.LDAP, auth.DLDAP:
config = parseLDAPConfig(form)
case auth.SMTP:
config = parseSMTPConfig(form)
case auth.PAM:
config = &pam_service.Source{
ServiceName: form.PAMServiceName,
EmailDomain: form.PAMEmailDomain,
}
case auth.OAuth2:
config = parseOAuth2Config(form)
oauth2Config := config.(*oauth2.Source)
if oauth2Config.Provider == "openidConnect" {
discoveryURL, err := url.Parse(oauth2Config.OpenIDConnectAutoDiscoveryURL)
if err != nil || (discoveryURL.Scheme != "http" && discoveryURL.Scheme != "https") {
ctx.Data["Err_DiscoveryURL"] = true
ctx.RenderWithErr(ctx.Tr("admin.auths.invalid_openIdConnectAutoDiscoveryURL"), tplAuthEdit, form)
return
}
}
default:
ctx.Error(http.StatusBadRequest)
return
}
source.Name = form.Name
source.IsActive = form.IsActive
source.IsSyncEnabled = form.IsSyncEnabled
source.Cfg = config
if err := auth.UpdateSource(ctx, source); err != nil {
if auth.IsErrSourceAlreadyExist(err) {
ctx.Data["Err_Name"] = true
ctx.RenderWithErr(ctx.Tr("admin.auths.login_source_exist", err.(auth.ErrSourceAlreadyExist).Name), tplAuthEdit, form)
} else if oauth2.IsErrOpenIDConnectInitialize(err) {
ctx.Flash.Error(err.Error(), true)
ctx.Data["Err_DiscoveryURL"] = true
ctx.HTML(http.StatusOK, tplAuthEdit)
} else {
ctx.ServerError("UpdateSource", err)
}
return
}
log.Trace("Authentication changed by admin(%s): %d", ctx.Doer.Name, source.ID)
ctx.Flash.Success(ctx.Tr("admin.auths.update_success"))
ctx.Redirect(setting.AppSubURL + "/admin/auths/" + strconv.FormatInt(form.ID, 10))
}
// DeleteAuthSource response for deleting an auth source
func DeleteAuthSource(ctx *context.Context) {
source, err := auth.GetSourceByID(ctx, ctx.ParamsInt64(":authid"))
if err != nil {
ctx.ServerError("auth.GetSourceByID", err)
return
}
if err = auth_service.DeleteSource(ctx, source); err != nil {
if auth.IsErrSourceInUse(err) {
ctx.Flash.Error(ctx.Tr("admin.auths.still_in_used"))
} else {
ctx.Flash.Error(fmt.Sprintf("auth_service.DeleteSource: %v", err))
}
ctx.JSONRedirect(setting.AppSubURL + "/admin/auths/" + url.PathEscape(ctx.Params(":authid")))
return
}
log.Trace("Authentication deleted by admin(%s): %d", ctx.Doer.Name, source.ID)
ctx.Flash.Success(ctx.Tr("admin.auths.deletion_success"))
ctx.JSONRedirect(setting.AppSubURL + "/admin/auths")
}

View file

@ -0,0 +1,62 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package oauth2
import (
"html/template"
"forgejo.org/modules/log"
"forgejo.org/modules/setting"
"forgejo.org/modules/svg"
"github.com/markbates/goth"
"github.com/markbates/goth/providers/openidConnect"
)
// OpenIDProvider is a GothProvider for OpenID
type OpenIDProvider struct{}
// Name provides the technical name for this provider
func (o *OpenIDProvider) Name() string {
return "openidConnect"
}
// DisplayName returns the friendly name for this provider
func (o *OpenIDProvider) DisplayName() string {
return "OpenID Connect"
}
// IconHTML returns icon HTML for this provider
func (o *OpenIDProvider) IconHTML(size int) template.HTML {
return svg.RenderHTML("gitea-openid", size, "tw-mr-2")
}
// CreateGothProvider creates a GothProvider from this Provider
func (o *OpenIDProvider) CreateGothProvider(providerName, callbackURL string, source *Source) (goth.Provider, error) {
scopes := setting.OAuth2Client.OpenIDConnectScopes
if len(scopes) == 0 {
scopes = append(scopes, source.Scopes...)
}
provider, err := openidConnect.New(source.ClientID, source.ClientSecret, callbackURL, source.OpenIDConnectAutoDiscoveryURL, scopes...)
if err != nil {
log.Warn("Failed to create OpenID Connect Provider with name '%s' with url '%s': %v", providerName, source.OpenIDConnectAutoDiscoveryURL, err)
}
return provider, err
}
// CustomURLSettings returns the custom url settings for this provider
func (o *OpenIDProvider) CustomURLSettings() *CustomURLSettings {
return nil
}
func (o *OpenIDProvider) CanProvideSSHKeys() bool {
return true
}
var _ GothProvider = &OpenIDProvider{}
func init() {
RegisterGothProvider(&OpenIDProvider{})
}

View file

@ -0,0 +1,59 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package oauth2
import (
"strings"
"forgejo.org/models/auth"
"forgejo.org/modules/json"
)
// Source holds configuration for the OAuth2 login source.
type Source struct {
Provider string
ClientID string
ClientSecret string
OpenIDConnectAutoDiscoveryURL string
CustomURLMapping *CustomURLMapping
IconURL string
Scopes []string
AttributeSSHPublicKey string
RequiredClaimName string
RequiredClaimValue string
GroupClaimName string
AdminGroup string
GroupTeamMap string
GroupTeamMapRemoval bool
RestrictedGroup string
SkipLocalTwoFA bool `json:",omitempty"`
// reference to the authSource
authSource *auth.Source
}
// FromDB fills up an OAuth2Config from serialized format.
func (source *Source) FromDB(bs []byte) error {
return json.UnmarshalHandleDoubleEncode(bs, &source)
}
// ToDB exports an SMTPConfig to a serialized format.
func (source *Source) ToDB() ([]byte, error) {
return json.Marshal(source)
}
// ProvidesSSHKeys returns if this source provides SSH Keys
func (source *Source) ProvidesSSHKeys() bool {
return len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0
}
// SetAuthSource sets the related AuthSource
func (source *Source) SetAuthSource(authSource *auth.Source) {
source.authSource = authSource
}
func init() {
auth.RegisterTypeConfig(auth.OAuth2, &Source{})
}

View file

@ -0,0 +1,88 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package forms
import (
"net/http"
"forgejo.org/modules/web/middleware"
"forgejo.org/services/context"
"code.forgejo.org/go-chi/binding"
)
// AuthenticationForm form for authentication
type AuthenticationForm struct {
ID int64
Type int `binding:"Range(2,7)"`
Name string `binding:"Required;MaxSize(30)"`
Host string
Port int
BindDN string
BindPassword string
UserBase string
UserDN string
AttributeUsername string
AttributeName string
AttributeSurname string
DefaultDomainName string
AttributeMail string
AttributeSSHPublicKey string
AttributeAvatar string
AttributesInBind bool
UsePagedSearch bool
SearchPageSize int
Filter string
AdminFilter string
GroupsEnabled bool
GroupDN string
GroupFilter string
GroupMemberUID string
UserUID string
RestrictedFilter string
AllowDeactivateAll bool
IsActive bool
IsSyncEnabled bool
SMTPAuth string
SMTPHost string
SMTPPort int
AllowedDomains string
SecurityProtocol int `binding:"Range(0,2)"`
TLS bool
SkipVerify bool
HeloHostname string
DisableHelo bool
ForceSMTPS bool
PAMServiceName string
PAMEmailDomain string
Oauth2Provider string
Oauth2Key string
Oauth2Secret string
OpenIDConnectAutoDiscoveryURL string
Oauth2UseCustomURL bool
Oauth2TokenURL string
Oauth2AuthURL string
Oauth2ProfileURL string
Oauth2EmailURL string
Oauth2IconURL string
Oauth2Tenant string
Oauth2Scopes string
Oauth2RequiredClaimName string
Oauth2RequiredClaimValue string
Oauth2GroupClaimName string
Oauth2AdminGroup string
Oauth2RestrictedGroup string
Oauth2GroupTeamMap string `binding:"ValidGroupTeamMap"`
Oauth2GroupTeamMapRemoval bool
Oauth2AttributeSSHPublicKey string
SkipLocalTwoFA bool
GroupTeamMap string `binding:"ValidGroupTeamMap"`
GroupTeamMapRemoval bool
}
// Validate validates fields
func (f *AuthenticationForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}

View file

@ -0,0 +1,18 @@
=== RUN TestRequiredClaimExactAnyOfAndNoMutation
--- PASS: TestRequiredClaimExactAnyOfAndNoMutation (0.00s)
=== RUN TestRequiredClaimConfigurationRefusalsAndLegacyIsolation
--- PASS: TestRequiredClaimConfigurationRefusalsAndLegacyIsolation (0.00s)
=== RUN TestCurrentUserInfoGuardRejectsMissingMalformedAndUnavailable
--- PASS: TestCurrentUserInfoGuardRejectsMissingMalformedAndUnavailable (0.03s)
=== RUN TestCurrentUserInfoEmptyArrayOverwritesAndGroupRemoval
--- PASS: TestCurrentUserInfoEmptyArrayOverwritesAndGroupRemoval (0.00s)
=== RUN TestCurrentUserInfoGuardIsOptInAndRedirectsRefuse
--- PASS: TestCurrentUserInfoGuardIsOptInAndRedirectsRefuse (0.00s)
=== RUN TestActualGothMergeRejectsOmissionProposal
--- PASS: TestActualGothMergeRejectsOmissionProposal (0.00s)
=== RUN TestActualGothCurrentGroupsOverrideStaleIDToken
--- PASS: TestActualGothCurrentGroupsOverrideStaleIDToken (0.00s)
=== RUN TestActualGothIdentityBindingAndLookupFailure
--- PASS: TestActualGothIdentityBindingAndLookupFailure (0.00s)
PASS
ok command-line-arguments 1.058s

View file

@ -0,0 +1,86 @@
package oauth2
import (
"encoding/json"
"testing"
"time"
)
func providerFor(body string, guarded bool) *Provider {
client := mockUserInfo(body, 200)
if guarded {
var err error
client, err = RequireCurrentUserInfoClaim(client, "https://issuer.example/userinfo", "groups")
if err != nil {
panic(err)
}
}
return &Provider{HTTPClient: client, ClientKey: "forgejo", OpenIDConfig: &OpenIDConfig{UserInfoEndpoint: "https://issuer.example/userinfo", Issuer: "https://issuer.example"}}
}
func TestActualGothMergeRejectsOmissionProposal(t *testing.T) {
// Retain the failed earlier proposal: omitted negative does NOT clear a
// previously issued positive. This is why no KeyCape omission patch ships.
p := providerFor(`{"sub":"alice","groups":[]}`, false)
claims := map[string]any{"sub": "alice", "forgejo_access": "allowed"}
if err := p.getUserInfo("synthetic", claims); err != nil {
t.Fatal(err)
}
if claims["forgejo_access"] != "allowed" {
t.Fatal("upstream merge behavior changed")
}
p = providerFor(`{"sub":"alice","groups":[],"forgejo_access":"denied"}`, false)
if err := p.getUserInfo("synthetic", claims); err != nil {
t.Fatal(err)
}
if claims["forgejo_access"] != "denied" {
t.Fatal("explicit negative did not replace positive")
}
}
func TestActualGothCurrentGroupsOverrideStaleIDToken(t *testing.T) {
for _, current := range [][]string{{}, {"unrelated"}, {"forge-admins"}, {"forge-coulomb-owners"}, {"forge-coulomb-readers"}, {"forge-admins", "forge-coulomb-readers"}} {
body, _ := json.Marshal(map[string]any{"sub": "alice", "groups": current})
p := providerFor(string(body), true)
claims := map[string]any{"sub": "alice", "groups": []string{"forge-admins"}, "preserved": "value"}
if err := p.getUserInfo("synthetic", claims); err != nil {
t.Fatal(err)
}
want := len(current) > 0 && current[0] != "unrelated"
if MatchRequiredClaimValues(claims, "groups", "", caseValues) != want {
t.Fatal("current group gate result differs")
}
if claims["preserved"] != "value" {
t.Fatal("unrelated ID claim lost")
}
}
// Removal and re-add are current lookups, independent of old ID positives.
for _, body := range []string{`{"sub":"alice"}`, `{"sub":"alice","groups":null}`, `{"sub":"alice","groups":"forge-admins"}`, `{"sub":"alice","groups":["forge-admins",false]}`} {
p := providerFor(body, true)
claims := map[string]any{"sub": "alice", "groups": []string{"forge-admins"}}
if p.getUserInfo("synthetic", claims) == nil {
t.Fatal("missing/malformed current data allowed ID fallback")
}
}
}
func TestActualGothIdentityBindingAndLookupFailure(t *testing.T) {
p := providerFor(`{"sub":"other-user","groups":["forge-admins"]}`, true)
if p.getUserInfo("synthetic", map[string]any{"sub": "alice"}) == nil {
t.Fatal("subject mismatch accepted")
}
p = providerFor(`{"groups":["forge-admins"]}`, true)
if p.getUserInfo("synthetic", map[string]any{"sub": "alice"}) == nil {
t.Fatal("missing subject accepted")
}
p = providerFor(`{"sub":"alice","groups":["forge-admins"]}`, true)
for _, claims := range []map[string]any{
{"aud": "other-client", "iss": "https://issuer.example", "exp": float64(time.Now().Add(time.Hour).Unix())},
{"aud": "forgejo", "iss": "https://other-issuer.example", "exp": float64(time.Now().Add(time.Hour).Unix())},
{"aud": "forgejo", "iss": "https://issuer.example", "exp": float64(time.Now().Add(-time.Hour).Unix())},
} {
if _, err := p.validateClaims(claims); err == nil {
t.Fatal("foreign/expired client claims accepted")
}
}
}

View file

@ -0,0 +1,54 @@
"""Render the real chart in temporary storage; never emit deployable values.
The deployed stock image remains incompatible. This is a source/flag compatibility
control, not an image build, workload change or accepted first-case projection.
"""
import sys
sys.dont_write_bytecode = True
import importlib.util
import json
from pathlib import Path
import re
import shlex
import subprocess
import tempfile
import yaml
ROOT=Path(__file__).resolve().parents[1]
packet=ROOT.parent/'apps-chart-projection-r1'
spec=importlib.util.spec_from_file_location('old_projection',packet/'tools/forgejo_chart_projection.py')
p=importlib.util.module_from_spec(spec);spec.loader.exec_module(p)
base=yaml.safe_load((packet/'preimages/helm/forgejo-values.yaml').read_bytes())
values=p.chart_projection(base,('synthetic-control',))
source=values['gitea']['oauth'][-1]
source.pop('requiredClaimValue')
source['requiredClaimValues']=','.join(p.CASE_GROUPS)
assert values['image']==base['image'] # Never pretend that an image was selected.
with tempfile.TemporaryDirectory(prefix='forgejo-new-flag-render-') as td:
t=Path(td);(t/'home').mkdir();f=t/'render-control.yaml';f.write_text(yaml.safe_dump(values))
env={'PATH':'/usr/bin:/bin','HOME':str(t/'home'),'HELM_CONFIG_HOME':str(t/'config'),
'HELM_CACHE_HOME':str(t/'cache'),'HELM_DATA_HOME':str(t/'data'),'HELM_PLUGINS':str(t/'plugins')}
r=subprocess.run(['/home/worsch/.local/bin/helm','template','forgejo',str(packet/'upstream/gitea-12.5.0.tgz'),
'--namespace','forgejo','-f',str(f),'-f',str(packet/'preimages/helm/forgejo-registry-values.yaml')],
cwd=td,env=env,capture_output=True,text=True,check=True)
docs=list(yaml.safe_load_all(r.stdout))
script=next(x['stringData']['configure_gitea.sh'] for x in docs if x and x['kind']=='Secret' and 'configure_gitea.sh' in x.get('stringData',{}))
argv=shlex.split(next(x.strip() for x in script.splitlines() if x.strip().startswith('gitea admin auth add-oauth')))
flags={a[2:] for a in argv if a.startswith('--')}
assert argv[argv.index('--required-claim-values')+1]==','.join(p.CASE_GROUPS)
assert '--required-claim-value' not in argv
owner_cli=(ROOT/'postimages/cmd/admin_auth_oauth.go').read_text()
known=set(re.findall(r'Name:\s*"([a-z0-9-]+)"',owner_cli))
assert not flags-known
assert 'RequiredClaimValues: c.StringSlice("required-claim-values")' in owner_cli
dep=next(x for x in docs if x and x['kind']=='Deployment')
init=next(c for c in dep['spec']['template']['spec']['initContainers'] if c['name']=='configure-gitea')
envmap={e['name']:e for e in init['env']}
assert envmap['GITEA_OAUTH_SECRET_0']['valueFrom']['secretKeyRef']=={'name':'forgejo-netkingdom-oidc-client','key':'CLIENT_SECRET'}
receipt={'successful':True,'chart':'12.5.0','candidate_cli_flags_match':True,
'group_values_exact':list(p.CASE_GROUPS),'existing_secret_reference_preserved':True,
'current_stock_image_supports_feature':False,'accepted_image_selected':False,
'deployable_values_emitted':False,'first_case_still_blocked':True,
'private_helper_executed':False,'production_calls':False}
(ROOT/'chart-validation.json').write_text(json.dumps(receipt,indent=2)+'\n')
print(json.dumps(receipt,sort_keys=True))

View file

@ -0,0 +1,71 @@
"""Compile the actual new source plus byte-extracted pinned Goth methods offline.
Only HTTP-client/provider dependency wiring is a local fixture. Merge, subject,
audience, issuer, expiry and HTTP decoding method bodies are upstream bytes.
No socket/native target, dependency download or ambient configuration is used.
"""
from pathlib import Path
import hashlib
import json
import shutil
import subprocess
import tempfile
ROOT=Path(__file__).resolve().parents[1]
POST=ROOT/'postimages'
upstream=ROOT/'evidence/goth-v1.80.0-openid-connect.go'
EXPECTED='274cfeb3bfcb5a09334fb7b7aa29b9806b41ca56f4d61b983bd2d98599a2fb0c'
body=upstream.read_text()
assert hashlib.sha256(upstream.read_bytes()).hexdigest()==EXPECTED
def extract(signature):
start=body.index(signature)
end=body.index('\n}',start)+2
return body[start:end]+'\n'
methods={name:extract(sig) for name,sig in {
'merge':'func (p *Provider) getUserInfo(',
'http':'func (p *Provider) fetchUserInfo(',
'claim':'func getClaimValue(',
'claim_list':'func getClaimValues(',
'json':'func unMarshal(',
'binding':'func (p *Provider) validateClaims(',
}.items()}
fixture='''package oauth2
import("bytes";"encoding/json";"errors";"fmt";"io/ioutil";"net/http";"time")
const subjectClaim="sub"
const audienceClaim="aud"
const issuerClaim="iss"
const expiryClaim="exp"
const clockSkew=10*time.Second
type OpenIDConfig struct{UserInfoEndpoint,Issuer string}
type Provider struct{HTTPClient *http.Client;OpenIDConfig *OpenIDConfig;SkipUserInfoRequest bool;ClientKey string}
func(p *Provider)Client()*http.Client{return p.HTTPClient}
'''+ '\n'.join(methods.values())
with tempfile.TemporaryDirectory(prefix='forgejo-claim-review-') as td:
tmp=Path(td)
for name in ['required_claim_values.go','required_claim_values_test.go']:
shutil.copyfile(POST/'services/auth/source/oauth2'/name,tmp/name)
shutil.copyfile(ROOT/'tests/merge_upstream_test.go',tmp/'merge_upstream_test.go')
(tmp/'goth_extracted_test.go').write_text(fixture)
(tmp/'home').mkdir()
env={'PATH':'/usr/bin:/bin','HOME':str(tmp/'home'),'GO111MODULE':'off',
'GOPROXY':'off','GOSUMDB':'off','GOTOOLCHAIN':'local',
'GOCACHE':'/tmp/forgejo-required-claim-values-r1-go-cache',
'GOPATH':str(tmp/'gopath')}
result=subprocess.run(['/usr/bin/go','test','-v','-race',
'required_claim_values.go','required_claim_values_test.go',
'goth_extracted_test.go','merge_upstream_test.go'],cwd=tmp,env=env,capture_output=True,text=True)
(ROOT/'test-output.txt').write_text(result.stdout+result.stderr)
print(result.stdout+result.stderr)
assert result.returncode==0
receipt={'successful':True,'test_functions':8,'race_detector':True,
'actual_source_compiled':['required_claim_values.go','required_claim_values_test.go'],
'upstream_extracted_method_hashes':{n:hashlib.sha256(v.encode()).hexdigest() for n,v in methods.items()},
'goth_version':'v1.80.0','production_calls':False,'network_sockets':False,
'dependency_downloads':False,'ambient_environment':False,
'full_forgejo_build':False,'deployed_image_unchanged':True,
'keycape_derived_claim_implemented':False,'architecture_approval':False}
(ROOT/'validation.json').write_text(json.dumps(receipt,indent=2)+'\n')
print(json.dumps(receipt,sort_keys=True))

View file

@ -0,0 +1,26 @@
{
"successful": true,
"test_functions": 8,
"race_detector": true,
"actual_source_compiled": [
"required_claim_values.go",
"required_claim_values_test.go"
],
"upstream_extracted_method_hashes": {
"merge": "edbb20746dc5539714ff1818083ef33315a59a09a7c5234d3c281a33bc7d6210",
"http": "683bd23a72b214660dce44435a475c65fad726f979dc7e7dc311ceb927d48c68",
"claim": "ff1020491f34c829af45e0313ff58225468c5a7a01ea2ce4387b0c18f0508570",
"claim_list": "3329bc53eba553ccd8c76f7e44338baed9a0b3a5d77e807c1cc77a25bc51eb22",
"json": "131c42eef4fc0cd4c7c5f36c44c38eafa059f5ff2d06df9e721d7307a789ea75",
"binding": "f2388ac16ba26ccd9898487edd837f70b53dc431d479d3edacad569f4faf008a"
},
"goth_version": "v1.80.0",
"production_calls": false,
"network_sockets": false,
"dependency_downloads": false,
"ambient_environment": false,
"full_forgejo_build": false,
"deployed_image_unchanged": true,
"keycape_derived_claim_implemented": false,
"architecture_approval": false
}

View file

@ -0,0 +1,12 @@
import json,subprocess,pathlib,time
r=pathlib.Path('/tmp/forgejo-independent-r1')
env=json.loads((r/'download-env.json').read_text());env.update(GOPROXY='off',GOSUMDB='off',CGO_ENABLED='1')
cmd=['bwrap','--unshare-net','--ro-bind','/usr','/usr','--ro-bind','/lib','/lib','--ro-bind','/lib64','/lib64','--symlink','usr/bin','/bin','--proc','/proc','--dev','/dev','--tmpfs','/tmp','--bind',str(r),str(r),'--chdir',str(r/'forgejo'),'--','env','-i']+[k+'='+v for k,v in env.items()]
commands=[('whole-backend-build',['go','build','-p','4','-tags','sqlite sqlite_unlock_notify','-o',str(r/'forgejo-review-binary'),'.']),('actual-package-tests',['go','test','-p','4','-tags','sqlite sqlite_unlock_notify','./services/auth/source/oauth2','-run','TestRequiredClaim|TestCurrentUserInfo','-count=1','-v'])]
results=[]
for name,args in commands:
start=time.time()
with (r/(name+'.log')).open('w') as out:result=subprocess.run(cmd+args,stdout=out,stderr=subprocess.STDOUT)
results.append({'name':name,'argv':args,'returncode':result.returncode,'seconds':round(time.time()-start,2),'network':'bwrap --unshare-net; GOPROXY=off GOSUMDB=off GOTOOLCHAIN=local'})
print(name,result.returncode,(r/(name+'.log')).read_text()[-4000:],flush=True)
(r/'build-results.json').write_text(json.dumps(results,indent=2))

View file

@ -0,0 +1,38 @@
[
{
"name": "whole-backend-build",
"argv": [
"go",
"build",
"-p",
"4",
"-tags",
"sqlite sqlite_unlock_notify",
"-o",
"/tmp/forgejo-independent-r1/forgejo-review-binary",
"."
],
"returncode": 0,
"seconds": 304.79,
"network": "bwrap --unshare-net; GOPROXY=off GOSUMDB=off GOTOOLCHAIN=local"
},
{
"name": "actual-package-tests",
"argv": [
"go",
"test",
"-p",
"4",
"-tags",
"sqlite sqlite_unlock_notify",
"./services/auth/source/oauth2",
"-run",
"TestRequiredClaim|TestCurrentUserInfo",
"-count=1",
"-v"
],
"returncode": 0,
"seconds": 37.39,
"network": "bwrap --unshare-net; GOPROXY=off GOSUMDB=off GOTOOLCHAIN=local"
}
]

View file

@ -0,0 +1,8 @@
import subprocess,pathlib,json
r=pathlib.Path('/tmp/forgejo-independent-r1')
env={'PATH':str(r/'go/bin')+':/usr/bin:/bin','HOME':str(r/'home'),'GOTOOLCHAIN':'local','GOPROXY':'https://proxy.golang.org','GOSUMDB':'sum.golang.org','GOPATH':str(r/'gopath'),'GOMODCACHE':str(r/'gomodcache'),'GOCACHE':str(r/'gocache'),'GOVCS':'*:off','GIT_CONFIG_NOSYSTEM':'1','GIT_CONFIG_GLOBAL':'/dev/null','GIT_TERMINAL_PROMPT':'0','GOMAXPROCS':'4'}
(r/'download-env.json').write_text(json.dumps(env,indent=2))
with (r/'download.log').open('w') as out:
result=subprocess.run([str(r/'go/bin/go'),'mod','download'],cwd=r/'forgejo',env=env,stdout=out,stderr=subprocess.STDOUT)
print('download result',result.returncode,flush=True)
print((r/'download.log').read_text()[-6000:])

View file

@ -0,0 +1,15 @@
{
"PATH": "/tmp/forgejo-independent-r1/go/bin:/usr/bin:/bin",
"HOME": "/tmp/forgejo-independent-r1/home",
"GOTOOLCHAIN": "local",
"GOPROXY": "https://proxy.golang.org",
"GOSUMDB": "sum.golang.org",
"GOPATH": "/tmp/forgejo-independent-r1/gopath",
"GOMODCACHE": "/tmp/forgejo-independent-r1/gomodcache",
"GOCACHE": "/tmp/forgejo-independent-r1/gocache",
"GOVCS": "*:off",
"GIT_CONFIG_NOSYSTEM": "1",
"GIT_CONFIG_GLOBAL": "/dev/null",
"GIT_TERMINAL_PROMPT": "0",
"GOMAXPROCS": "4"
}

View file

@ -0,0 +1,7 @@
{
"forgejo_archive_sha256": "1ddd8c926ca07d40436897971458ace7dcf1d3496a0813ab9e0d869edef2365c",
"forgejo_url": "https://codeberg.org/forgejo/forgejo/archive/v11.0.3.tar.gz",
"go_url": "https://go.dev/dl/go1.24.3.linux-amd64.tar.gz",
"go_official_sha256": "3333f6ea53afa971e9078895eaa4ac7204a8c6b5c68c10e6bc9a33e8e391bdd8",
"all_pre_postimages_exact": true
}

View file

@ -0,0 +1,124 @@
{
"schema": "custodian.forgejo-required-claim-values-independent-review.v1",
"reviewed_on": "2026-10-17",
"verdict": "accept exact source implementation for owner source integration review; no blocking source defect identified",
"candidate_patch_sha256": "f2788b3f2bf84dbc0f94e232b6c9614a864b8123c09f9e44703d50d5e1b53257",
"reviewed_outputs": [
{
"path": "cmd/admin_auth_oauth.go",
"sha256": "1dd03f0bd23889a340a10fcc18d2e3a49270b79056aafd1b3baf01b18559b37f"
},
{
"path": "routers/web/admin/auths.go",
"sha256": "3498229e06fb9ff52396bcb5e68c0cc2d2ad24e6494533d8717d7c833f3e2923"
},
{
"path": "routers/web/auth/oauth.go",
"sha256": "081bcb94023d6e27289196bbe518bebba0a8a6082ea1a32d2794c5562075907b"
},
{
"path": "services/auth/source/oauth2/providers_openid.go",
"sha256": "5dd9b906cce434c774968d57fcc790cb678147345fea4d2e58477ec6eed70bcf"
},
{
"path": "services/auth/source/oauth2/required_claim_values.go",
"sha256": "29f16d07464bbe12e3f65616b6c1eb60561d04cd8ff89ecbfb56f93dbeaf1cda"
},
{
"path": "services/auth/source/oauth2/required_claim_values_test.go",
"sha256": "54a8bd97dc51c524a9adf0c6aefb9a19fa152d234b3bb929bf1a90932662d3d0"
},
{
"path": "services/auth/source/oauth2/source.go",
"sha256": "44e99883c2ec479427b3f72c73353fe33936eed2e5c962b2fa40803576aa5642"
}
],
"all_33_frozen_packet_hashes_verified": true,
"frozen_original_unchanged": true,
"upstream": {
"forgejo_archive_sha256": "1ddd8c926ca07d40436897971458ace7dcf1d3496a0813ab9e0d869edef2365c",
"forgejo_url": "https://codeberg.org/forgejo/forgejo/archive/v11.0.3.tar.gz",
"go_url": "https://go.dev/dl/go1.24.3.linux-amd64.tar.gz",
"go_official_sha256": "3333f6ea53afa971e9078895eaa4ac7204a8c6b5c68c10e6bc9a33e8e391bdd8",
"all_pre_postimages_exact": true,
"archive_pax_commit": "b81aa4d0807cc1e2d6a2fbafd0b75c46cc0ae237",
"go_mod_sum_unchanged": true
},
"current_review_inputs": {
"apps_head": "944b30ad63ab054cc24617e374e177d34cd572da",
"nk_packet_head": "befb0bc68cd25b625804fce559476eefd702521d",
"nk_current_head": "0241070b914d62b6ee483d61d587ee9a8e7152f0",
"current_NK_security_companion_matches_packet": true,
"Apps_AGENTS_INTENT_read": true
},
"compilation": {
"whole_actual_patched_backend_executable": "PASS",
"actual_source_not_extracted_method_fixture": true,
"argv": [
"go",
"build",
"-p",
"4",
"-tags",
"sqlite sqlite_unlock_notify",
"-o",
"/tmp/forgejo-independent-r1/forgejo-review-binary",
"."
],
"binary_bytes": 132365872,
"binary_sha256": "b918c080d6ee00b3e5ee459ef1ce4e40c6268339c3b6e36a12d303b15f41fc79",
"toolchain": "official Go1.24.3 under /tmp, digest independently verified",
"elapsed_seconds": 304.79,
"frontend_release_container_image_build": false,
"notes": "Read upstream Makefile backend/generate/release shapes. Direct root Go build proves the full backend dependency graph and executable, without bindata/frontend assets or image provenance. No binary copied into canonical repository or published."
},
"tests": {
"packet_source_extracted_Goth_race_tests": 8,
"packet_tests_reproduced_network_disabled": true,
"actual_oauth2_package_candidate_tests": 5,
"independent_actual_package_test_functions": 3,
"independent_named_subtests": 14,
"independent_controls": [
"actual UrfaveCLI nil default retains legacy",
"actual old singular comma literal unchanged",
"actual new chart comma and repeated flags parse expected list",
"duplicate/empty/conflicting new CLI configuration refused",
"actual Source ToDB/FromDB preserves extension, synthetic secret, group/admin/scopes and other configuration",
"legacy Source remains nil-list; malformed scalar persisted list refuses decoding",
"escaped duplicate JSON keys and duplicate subject refused",
"unrelated nested JSON preserved byte-for-byte",
"case lookalike never intersects allowset",
"null entry, invalid Unicode surrogate and over1024 group entries refused",
"body over1MiB and POST endpoint refused"
],
"first_actual_cmd_attempt": "Harness failed before selected cmd test: GIT_CONFIG_GLOBAL=/dev/null prevented upstream TestMain git.Init writing test-only config. Retained failure log/results; reran under same network boundary with an initially absent /tmp empty-home reviewer.gitconfig. All selected cmd/oauth2 tests then passed. No candidate correction needed.",
"chart_render": "PASS chart12.5.0; exact three-group flag shape and CLIENT_SECRET reference preserved"
},
"isolation": {
"public_dependencies": "allowlisted env; empty HOME; GOPROXY=https://proxy.golang.org; GOSUMDB=sum.golang.org; GOVCS=*:off; GOTOOLCHAIN=local; no automatic toolchain download",
"decisive_build_tests": "bwrap --unshare-net; no host home/config; GOPROXY=off GOSUMDB=off; fixed Go1.24.3; fixture race tests use existing Go1.22.2; in-memory HTTP transport; local SQLite setup only for upstream cmd TestMain",
"private_Git_SSH_config_credentials_native_model_image_publication_calls": false
},
"behavior_review": {
"new_gate": "Exact intersection of current incoming group array with configured original three-group allowset; no claim mutation or issuer forgejo_access shortcut. Missing/null/scalar/mixed/malformed/duplicate top-level JSON refuses; empty current array overwrites stale ID-token groups then denies. Duplicate configured literals refuse; duplicate incoming array members do not add authority.",
"legacy": "Nil-list config uses unchanged singular callback, including literal commas; regular providers and local sign-in not rewired.",
"update_UI": "CLI replaces list only if flag explicitly set; omission preserves guarded list. Empty/new conflicting fields refuse. UI POST on guarded source returns error before reconstructed source/config writes; it cannot erase list. No new clear/disable/recovery capability added.",
"Goth": "Guard observes successful current HTTPS UserInfo before Goth merges; original bytes retained. Retained actual upstream methods prove stale omission issue and subject/audience/issuer/expiry failures. All normal identity validation remains upstream.",
"architectural_limit": "Existing/new consumer gate itself is still an authorization enforcement architecture question under current NK SECURITY-COMPANION3.1-3.2 and Apps INTENT. Technical passing tests are not estate architecture acceptance. No derived KeyCape claim/model/config change or single-PDP exception is admitted."
},
"inverse_and_release_gates": [
"No accepted replacement image, reproducible release/frontend/container build, registry publication or deployed compatibility established; stock11.0.3 cannot accept new flag.",
"Helm chart omission leaves persisted OAuth source; old binary ignores unknown RequiredClaimValues field and presence-only singular config weakens gate. Downgrade before authenticated application-source inverse is unsafe.",
"Existing source identity/config/secret preservation, exact readback/CAS/writer exclusion, durable authenticated journal, current operation and inverse sessions remain unimplemented owner capability contracts.",
"Linked users or external logins cause existing DeleteSource refusal; never delete/detach users or preexisting/shared sources to force inverse. Retain extended image and reconcile.",
"Actual runtime chart CLIENT_SECRET expands into --secret argv; reference-only source does not prove process/audit/crash confidentiality. Owner admission remains required.",
"Real browser/consumer probe, source persistence and inverse operation evidence, architecture acceptance, owner formal assent, exact type/case/descriptor re-freeze and image admission remain separate."
],
"formal_owner_assent": false,
"architecture_acknowledgment": false,
"image_admission": false,
"case_or_type_refreeze": false,
"first_case_still_blocked": true,
"correction_patch": "none; no blocking implementation flaw found",
"T37_complete": false
}

View file

@ -0,0 +1,19 @@
{
"argv": [
"go",
"test",
"-p",
"4",
"-tags",
"sqlite sqlite_unlock_notify",
"./cmd",
"./services/auth/source/oauth2",
"-run",
"TestReviewer",
"-count=1",
"-v"
],
"returncode": 1,
"seconds": 21.66,
"network": "disabled by bwrap"
}

View file

@ -0,0 +1,19 @@
{
"argv": [
"go",
"test",
"-p",
"4",
"-tags",
"sqlite sqlite_unlock_notify",
"./cmd",
"./services/auth/source/oauth2",
"-run",
"TestReviewer",
"-count=1",
"-v"
],
"returncode": 0,
"seconds": 9.86,
"network": "disabled by bwrap"
}

View file

@ -0,0 +1,8 @@
import json,subprocess,pathlib,time
r=pathlib.Path('/tmp/forgejo-independent-r1');env=json.loads((r/'download-env.json').read_text());env.update(GOPROXY='off',GOSUMDB='off',CGO_ENABLED='1',GIT_CONFIG_GLOBAL=str(r/'home/reviewer.gitconfig'))
base=['bwrap','--unshare-net','--ro-bind','/usr','/usr','--ro-bind','/lib','/lib','--ro-bind','/lib64','/lib64','--symlink','usr/bin','/bin','--proc','/proc','--dev','/dev','--tmpfs','/tmp','--bind',str(r),str(r),'--chdir',str(r/'forgejo'),'--','env','-i']+[k+'='+v for k,v in env.items()]
args=['go','test','-p','4','-tags','sqlite sqlite_unlock_notify','./cmd','./services/auth/source/oauth2','-run','TestReviewer','-count=1','-v']
start=time.time()
with (r/'reviewer-actual-package-tests.log').open('w') as out:result=subprocess.run(base+args,stdout=out,stderr=subprocess.STDOUT)
(r/'reviewer-test-results.json').write_text(json.dumps({'argv':args,'returncode':result.returncode,'seconds':round(time.time()-start,2),'network':'disabled by bwrap'},indent=2))
print('reviewer actual package tests',result.returncode,flush=True);print((r/'reviewer-actual-package-tests.log').read_text()[-8000:])

View file

@ -0,0 +1,30 @@
package cmd
import (
"reflect"
"testing"
"forgejo.org/services/auth/source/oauth2"
"github.com/urfave/cli/v2"
)
func TestReviewerActualCLIListAndLegacyParsing(t *testing.T) {
tests:=[]struct {name string;args []string;want []string;legacy string;invalid bool}{
{name:"unset keeps nil",args:[]string{"--provider","openidConnect"}},
{name:"old comma literal unchanged",args:[]string{"--required-claim-name","groups","--required-claim-value","forge-admins,forge-coulomb-owners"},legacy:"forge-admins,forge-coulomb-owners"},
{name:"chart comma list",args:[]string{"--provider","openidConnect","--required-claim-name","groups","--required-claim-values","forge-admins,forge-coulomb-owners,forge-coulomb-readers"},want:[]string{"forge-admins","forge-coulomb-owners","forge-coulomb-readers"}},
{name:"repeated list",args:[]string{"--required-claim-name","groups","--required-claim-values","forge-admins","--required-claim-values","forge-coulomb-readers"},want:[]string{"forge-admins","forge-coulomb-readers"}},
{name:"duplicate flag list refused",args:[]string{"--required-claim-name","groups","--required-claim-values","forge-admins,forge-admins"},want:[]string{"forge-admins","forge-admins"},invalid:true},
{name:"explicit empty refused",args:[]string{"--required-claim-name","groups","--required-claim-values",""},want:[]string{""},invalid:true},
{name:"conflicting singular refused",args:[]string{"--required-claim-name","groups","--required-claim-value","old","--required-claim-values","forge-admins"},legacy:"old",want:[]string{"forge-admins"},invalid:true},
}
for _,tt:=range tests {t.Run(tt.name,func(t *testing.T){
app:=cli.NewApp();app.Flags=oauthCLIFlags
app.Action=func(ctx *cli.Context)error {source:=parseOAuth2Config(ctx)
if !reflect.DeepEqual(source.RequiredClaimValues,tt.want)||source.RequiredClaimValue!=tt.legacy {t.Fatalf("CLI parse mismatch: list=%q legacy=%q",source.RequiredClaimValues,source.RequiredClaimValue)}
err:=oauth2.ValidateRequiredClaimValues(source.RequiredClaimName,source.RequiredClaimValue,source.RequiredClaimValues)
if (err!=nil)!=tt.invalid {t.Fatalf("config validation mismatch: %v",err)}
return nil
}
if err:=app.Run(append([]string{"review"},tt.args...));err!=nil {t.Fatal(err)}
})}
}

View file

@ -0,0 +1,55 @@
package oauth2
import (
"bytes"
"encoding/json"
"io"
"net/http"
"reflect"
"strings"
"testing"
)
func TestReviewerSourcePersistenceAndLegacy(t *testing.T) {
original:=&Source{Provider:"openidConnect", ClientID:"forgejo", ClientSecret:"synthetic-private-value", RequiredClaimName:"groups", RequiredClaimValues:[]string{"forge-admins","forge-coulomb-owners","forge-coulomb-readers"}, AdminGroup:"forge-admins", GroupClaimName:"groups",Scopes:[]string{"openid","groups"}}
raw,err:=original.ToDB(); if err!=nil {t.Fatal(err)}
var decoded Source; if err=decoded.FromDB(raw);err!=nil {t.Fatal(err)}
if !reflect.DeepEqual(original,&decoded) {t.Fatal("persisted extension or unrelated source config changed")}
var legacy Source
if err=legacy.FromDB([]byte(`{"Provider":"openidConnect","RequiredClaimName":"groups","RequiredClaimValue":"legacy,literal"}`));err!=nil {t.Fatal(err)}
if legacy.RequiredClaimValues!=nil || legacy.RequiredClaimValue!="legacy,literal" {t.Fatal("legacy semantics changed")}
if err=ValidateRequiredClaimValues(legacy.RequiredClaimName,legacy.RequiredClaimValue,legacy.RequiredClaimValues);err!=nil {t.Fatal(err)}
if err=decoded.FromDB([]byte(`{"RequiredClaimValues":"forge-admins"}`));err==nil {t.Fatal("scalar persisted list accepted")}
}
func TestReviewerGuardBoundaryControls(t *testing.T) {
endpoint:="https://issuer.example/userinfo"
cases:=[]struct {name,body string; allowed bool}{
{"escaped duplicate key",`{"groups":[],"gro\u0075ps":["forge-admins"]}`,false},
{"duplicate subject",`{"sub":"alice","sub":"bob","groups":["forge-admins"]}`,false},
{"unknown nested JSON allowed",`{"sub":"alice","unrelated":{"a":1},"groups":["forge-coulomb-readers"]}`,true},
{"exact case","{\"groups\":[\"Forge-admins\"]}",true},
{"null entry",`{"groups":["forge-admins",null]}`,false},
{"unicode surrogate",`{"groups":["forge-admins","\ud800"]}`,false},
{"too many groups",`{"groups":[`+strings.TrimSuffix(strings.Repeat(`"forge-admins",`,1025),",")+`]}`,false},
}
for _,tt:=range cases {t.Run(tt.name,func(t *testing.T){
client,err:=RequireCurrentUserInfoClaim(mockUserInfo(tt.body,200),endpoint,"groups");if err!=nil {t.Fatal(err)}
response,err:=client.Get(endpoint)
if (err==nil)!=tt.allowed {t.Fatalf("allowed=%v err=%v",tt.allowed,err)}
if err==nil {b,_:=io.ReadAll(response.Body);response.Body.Close();if !bytes.Equal(b,[]byte(tt.body)){t.Fatal("response modified")}
var claims map[string]any;if json.Unmarshal(b,&claims)!=nil {t.Fatal("invalid test body")}
matched:=MatchRequiredClaimValues(claims,"groups","",caseValues)
if matched!=(tt.name=="unknown nested JSON allowed") {t.Fatal("consumer allowset mismatch")}
}
})}
// Upper bounds are enforced without truncation; an allowed value cannot rescue malformed data.
if MatchRequiredClaimValues(map[string]any{"groups":[]any{"forge-admins",true}},"groups","",caseValues) {t.Fatal("mixed malformed claim admitted")}
// Body limit must refuse before a trailing allowed group could be trusted.
client,_:=RequireCurrentUserInfoClaim(mockUserInfo(`{"padding":"`+strings.Repeat("a",maxUserInfoBytes)+`","groups":["forge-admins"]}`,200),endpoint,"groups")
if response,err:=client.Get(endpoint);err==nil {response.Body.Close();t.Fatal("oversized body admitted")}
// POST at the exact endpoint must not bypass method guard.
client,_=RequireCurrentUserInfoClaim(mockUserInfo(`{"groups":["forge-admins"]}`,200),endpoint,"groups")
req,_:=http.NewRequest(http.MethodPost,endpoint,nil)
if response,err:=client.Do(req);err==nil {response.Body.Close();t.Fatal("unexpected method admitted")}
}

View file

@ -0,0 +1,35 @@
{
"status": "complete",
"phase": "review frozen; all owned exec sessions completed; whole backend compile and selected tests passed",
"exec_session_id": null,
"driver": "/tmp/forgejo-independent-r1/build-offline.py",
"workspace": "/tmp/forgejo-independent-r1/forgejo",
"log": "/tmp/forgejo-independent-r1/whole-backend-build.log",
"log_progress": "Go build cache grew from 156MB to644MB without diagnostics; do not restart on observation timeout",
"next_driver": "/tmp/forgejo-independent-r1/reviewer-tests-offline.py",
"completed": [
"33 frozen packet hashes verified",
"all official archive preimages/postimages verified; archive PAX commit b81aa4d0807cc1e2d6a2fbafd0b75c46cc0ae237",
"official Go1.24.3 digest verified",
"public dependency acquisition successful with fixed proxy+sumdb and VCS disabled; go.mod/go.sum unchanged",
"8 packet source/Goth fixture tests pass with race detector",
"chart12.5.0 render reproduced"
],
"current_Apps_head": "944b30ad63ab054cc24617e374e177d34cd572da",
"current_NK_head": "0241070b914d62b6ee483d61d587ee9a8e7152f0",
"NK_companion_equals_frozen_packet": true,
"full_compile_claimed": true,
"formal_owner_assent": false,
"frozen_original_unchanged": true,
"timestamp_utc": "2026-10-17T21:14:53.967922+00:00",
"full_backend_compile_succeeded": true,
"frontend_container_image_build_claimed": false,
"reviewer_tests_exec_session_id": null,
"reviewer_tests_log": "/tmp/forgejo-independent-r1/reviewer-actual-package-tests.log",
"completed_exec_sessions": [
58181,
83343,
97349,
10513
]
}

View file

@ -0,0 +1,774 @@
{
"schema": "statehub.guard-image-inspection.v1",
"status": "verified-selected-public-image-bytes",
"source_commit": "a25b62f75aab6165ee9872ee53ac90c679ec746a",
"tag": "main-a25b62f",
"image_digest": "sha256:226086b1acaec3fc77e30014a101c96de687e38c9eb52cd522e5e69efb1980a0",
"selected_source_hashes": {
"app/api/services/historical_projection.py": "sha256:e24e57c58f4aaa4ec9d9bf088009ac69006e53f75b9477ee53dc26c4e850b975",
"app/api/routers/workstreams.py": "sha256:9c9bf4376dd323b52109912bb3f615949e89ac02c8b88b4720f569f8565ad1d6",
"app/scripts/consistency_check.py": "sha256:eaee916f46a11f49d2621fc9386717d73b7fd517acbe4b695e7b551d67bfa8f7",
"app/api/services/forge_projection.py": "sha256:b980e62cbd85c3e7df42b01abf14354e04133374db94af6531dcd91629c3487d",
"app/migrations/versions/c4d5e6f7a8b9_inbox_feed_revision.py": "sha256:48f1730037be99b5623e878d7f20a06bec243ce27317fde81c82e16006d7b3ce",
"tmp/.ci-build-id": "sha256:ea57ead67d610c44a6237a7d96be66a8f9882f026de544b8efc69dbd7d57315d"
},
"observed_files": {
"tmp/.ci-build-id": "sha256:ea57ead67d610c44a6237a7d96be66a8f9882f026de544b8efc69dbd7d57315d",
"tmp/hub-core/hub_core/__init__.py": "sha256:098578bae76f7d97354b8f3dfe8ddc63b15806a46d1691d70ed12d9839d69aec",
"tmp/hub-core/hub_core/conformance/__init__.py": "sha256:f7ce0a651913f157e586afd6e953b483add0dd5257ee9f2406c47e8d46ee9587",
"tmp/hub-core/hub_core/conformance/harness.py": "sha256:ca870fbbec7b7f5db6521d615fd76dfcbb9e97cbdfb037ffa993851a663b3c40",
"tmp/hub-core/hub_core/contracts/__init__.py": "sha256:ffb7a6b79ce15ddd57943bfbc40799e078c6f57c6690018237baa3f3a755ac49",
"tmp/hub-core/hub_core/contracts/helixforge_hub_extension/__init__.py": "sha256:32c095a16f777127ec0d2cd5e8373889866b81e9955d01e5adaf9503f6864da4",
"tmp/hub-core/hub_core/contracts/helixforge_hub_extension/v0_1_0/__init__.py": "sha256:e176b86b24d79c11f78ad714878bc3fca41c7821654fa766278b697360a41201",
"tmp/hub-core/hub_core/contracts/repository_navigation/__init__.py": "sha256:64912fe6673b72c3c0d8a2da28673d58bdf6f4185a1376ffe43d3735350adb60",
"tmp/hub-core/hub_core/contracts/repository_navigation/v1_0_0/__init__.py": "sha256:574cf1fee8433e0b11d9475cfbcc3837730b55f6ea1faa4920cba106d959464f",
"tmp/hub-core/hub_core/contracts/workload_projection/__init__.py": "sha256:c01198d23571442c1c3a596c7cd16be17dc1a3efdcfe3e8f9725b7dba16fc103",
"tmp/hub-core/hub_core/contracts/workload_projection/v1_0_0/__init__.py": "sha256:a9ecfecbf44893d32268288c872cdede0440d2a1373d37a47f69ba8c6431de0d",
"tmp/hub-core/hub_core/database.py": "sha256:a7704e97509433e90655b0ea22d5fa626d75b8b6470015ef8b9e5997e7a7f5ea",
"tmp/hub-core/hub_core/events.py": "sha256:c7125a242ed638a3ede29129e4ad826cd188cf76239b59a8587a40d182bbd0e7",
"tmp/hub-core/hub_core/mcp/__init__.py": "sha256:f97b25193573e56886b66f91cb8c86ef4a39f2100d709869e279e17456959859",
"tmp/hub-core/hub_core/mcp/credentials.py": "sha256:f5433d29848bd5a399a5e31695e0d908748e0a0856d26b2d691b667c50c95c34",
"tmp/hub-core/hub_core/mcp/server.py": "sha256:fc6a6de0a29ad9b6da176302a7d24e4ba987f667b6a7fb001029f00cb0d0c582",
"tmp/hub-core/hub_core/message_identity.py": "sha256:0ab996ba50f236c1342ce72ca8ae6ff28102ef41216972a745f7260e0c93fb24",
"tmp/hub-core/hub_core/migrations/__init__.py": "sha256:ccf4820e270c97fd5d4ac02f0c96362a772b8f4d1bcccc3f5e0077b77f575d36",
"tmp/hub-core/hub_core/migrations/env.py": "sha256:282e6e1a9e4a3cfdc111887f04dc5c1645307b3943a421852abe1709ce712e57",
"tmp/hub-core/hub_core/migrations/roles.py": "sha256:91e85297d296b0c4b9c78b16ae67d3a4707232d8f32b2cac2b2e84f31fa80e89",
"tmp/hub-core/hub_core/migrations/versions/0001_core_schema.py": "sha256:c65f076a7b56fb93ee67869e04d922ea396eab72a4c2d2f0dabd706c918aa081",
"tmp/hub-core/hub_core/migrations/versions/0002_runtime_ports.py": "sha256:d3ce15463841b3c13eaafdd137a65d4cf2839c0b8e8fddcb2cb2f1adbceb9f00",
"tmp/hub-core/hub_core/migrations/versions/0003_repository_navigation.py": "sha256:fb2d2cb5ed10f1760ab789ced8d43c657c1bedc29ba666231a70a18636160902",
"tmp/hub-core/hub_core/migrations/versions/0004_workload_projection.py": "sha256:87ce668bfef5cf4aaf0cfd3b48d498f2370f68e06a6e66375e446af6fbbb9c96",
"tmp/hub-core/hub_core/migrations/versions/0005_message_identity_aliases.py": "sha256:ec05d2c3cb906b78cdec4d220a7ce09264c38904c792cdad3cd1082e7eace7c0",
"tmp/hub-core/hub_core/migrations/versions/0006_outcome_outbox.py": "sha256:05effdf6f258836d1cd223f9d9fc3447d264484021d3a51cfa42517453d613e1",
"tmp/hub-core/hub_core/migrations/versions/__init__.py": "sha256:569f6d7e5cc1c17b7b1eee3eec3b82774b745d01ee1dfdf851a5c6f5c337f0bb",
"tmp/hub-core/hub_core/models/__init__.py": "sha256:c0b6de4986614a163dbb2467d15882eadbd66800478a50de4d6404198d1f773a",
"tmp/hub-core/hub_core/models/agent_message.py": "sha256:23d16eeaffe780f326b08e6c441100e6ae7b4075bbb6421734c06156a87c4375",
"tmp/hub-core/hub_core/models/base.py": "sha256:85bb40bde6f4cf8e6c5700a3a016a711fab4adff77c5eb78f89827fe28b9537b",
"tmp/hub-core/hub_core/models/capability_catalog.py": "sha256:eebbb31f264821a61662d3c9e8c7eba74f46d7f68b946e35f8127343bf2ff9c4",
"tmp/hub-core/hub_core/models/capability_request.py": "sha256:70a867c82aaa061a4e1c671b023d213032464e776226280f99e2ca192e2a6eba",
"tmp/hub-core/hub_core/models/domain.py": "sha256:ddf45c37e3c37014ddd9148be01cf104658e1601abba2a8ba96f7680cdb581b3",
"tmp/hub-core/hub_core/models/managed_repo.py": "sha256:835c5d44581daf7d7dd95325e3f7fdf0add4d529b83779ed83b5e812dcb3aa61",
"tmp/hub-core/hub_core/models/message_identity_alias.py": "sha256:d07d59cd9c9351669e8319727d08bc0e9bb7e0ee36bf8a34c0c258f0b4ea61e4",
"tmp/hub-core/hub_core/models/progress_event.py": "sha256:ee750127db2f49c5b9b94773a408f7f965d834b02e6d962220f4bc762309161f",
"tmp/hub-core/hub_core/models/tpsc.py": "sha256:cb28cbe3512486adb73c0bce633b3c542a41a64e7ec673e86a5a25ab20d94afe",
"tmp/hub-core/hub_core/routers/__init__.py": "sha256:800d1c45f597fcb4970ca7a83acd2b36016006efbdf638e14a3482a954d9046d",
"tmp/hub-core/hub_core/routers/capabilities.py": "sha256:18a403ad6ac961bc490543b698e079336f60ef9902a6cd71ef9899076660d5c0",
"tmp/hub-core/hub_core/routers/domains.py": "sha256:d4c4061d2ab0d2d6390a10f4f815deb0b93b3979345aee965b666110ba32e6e0",
"tmp/hub-core/hub_core/routers/messages.py": "sha256:903bd4f8c8ca599b606cf2229315de21097360816fcda907159dac4f48cee512",
"tmp/hub-core/hub_core/routers/policy.py": "sha256:358271263295fdebb5bcb95c3a27bd3ea456964a77bc01c3f784fb7a8754aacf",
"tmp/hub-core/hub_core/routers/progress.py": "sha256:680cf4dea00a1734d7d70ef7880acc8fd39f045521baa47f415c17934779d18a",
"tmp/hub-core/hub_core/routers/repos.py": "sha256:1bca6c9b2b5bbe172f5b935b763ac1e399b1d403cdc2c463000ae79f843257a2",
"tmp/hub-core/hub_core/routers/tpsc.py": "sha256:706815b01be1c0320bf73cf9cff3e9415c1f27b8449ee9e6fea67b12d3042fd1",
"tmp/hub-core/hub_core/runtime/__init__.py": "sha256:c522e626373407bf9bcd727dac7bb812cbdc8c3ef5bb6509a74949e5b49ab4c2",
"tmp/hub-core/hub_core/runtime/app.py": "sha256:87b077498bbe321c73139e51f7ca689fdc7963a98c4d72a5a3bbcc564f47ee76",
"tmp/hub-core/hub_core/runtime/cli.py": "sha256:98ec00f2a12911245827f515877472ae1c37cea65a42454494450ffbe7380e57",
"tmp/hub-core/hub_core/runtime/compat.py": "sha256:9f1c4575bb406ae1174da1a005ec16f44ace63768617e11164b44b4051162d0d",
"tmp/hub-core/hub_core/runtime/compat_catalogs.py": "sha256:250f61ea39cced32c8cd932c4e770ae8be54ff1cf1e257ee2c82324bde0d7a26",
"tmp/hub-core/hub_core/runtime/config.py": "sha256:009b2fc5d1f8fc390d478b01444f610ada91e5fee30534936382be2529a843fc",
"tmp/hub-core/hub_core/runtime/inbox_projection.py": "sha256:84499238654310b86f7a97f2817163ff4c4da0dfce562c677294fa3dfef7f398",
"tmp/hub-core/hub_core/runtime/migration.py": "sha256:d12731fa581c48bf58deec19405ece977c89d08993b2a2ad047cb1fd17318ea8",
"tmp/hub-core/hub_core/runtime/models.py": "sha256:367ca4aa29f59f8e3f59eab22f12782146cf623fa7ac7c22346c794b85a88185",
"tmp/hub-core/hub_core/runtime/ports.py": "sha256:ab70a401deae73389761a0e88dc8ee843df00a671fae6919b1f7eed344cfb542",
"tmp/hub-core/hub_core/runtime/postgres_store.py": "sha256:9286e7845009b66f00069d67177733dabe372d79986c33ca9b43e00dfa5a2341",
"tmp/hub-core/hub_core/runtime/repo_manager_client.py": "sha256:8fa305277c5f3ddfdbe1daf4023ce6c3cde03b5c18c1292dd6aac3fe995aabe1",
"tmp/hub-core/hub_core/runtime/repository_navigation.py": "sha256:f3503596781f3e7f7d53dd28dd3aaf563905f1398575b40c8080e2b1b9978b1e",
"tmp/hub-core/hub_core/runtime/repository_navigation_routes.py": "sha256:eab78ab418a7b542dcdcab5da023bacc65fb50520853f8d179accb2105e373cf",
"tmp/hub-core/hub_core/runtime/store.py": "sha256:2fb78f14bab8df08807322d6f6416745856d69e6001381246abbac879891858d",
"tmp/hub-core/hub_core/runtime/tables.py": "sha256:a5ff72a9c54eedcc8bb81330e62ee72578a383a1d03a1acb598c72c96cdfbe9e",
"tmp/hub-core/hub_core/runtime/validation.py": "sha256:2add804d95d5d0469d6d8d605fbd1013885da972489a017260c6fddef7b80c79",
"tmp/hub-core/hub_core/runtime/workload_projection.py": "sha256:8500260404b0c566a62e51fef63ee320d4e3dafe5e2467f44db884296b9e3bf4",
"tmp/hub-core/hub_core/runtime/workload_projection_routes.py": "sha256:5c71189b98867f2f1f7612f47ee69c0ec3ddf836b9b1c7e134b98a2be45791de",
"tmp/hub-core/hub_core/schemas/__init__.py": "sha256:dfa2ec2d125f03a532637d2fbef3058ebe1f85c515ff42a930198e164b7812f7",
"tmp/hub-core/hub_core/schemas/agent_message.py": "sha256:d6a7f40dc9a3c61be761f4d2e2c08f726fb0a423ef73e273a7cd97c69ae6c6a3",
"tmp/hub-core/hub_core/schemas/capability.py": "sha256:949c55cb1f2694de4617ad53dc9cb7bc0e72addd7b9d0f104b93d783e4edc647",
"tmp/hub-core/hub_core/schemas/doi.py": "sha256:73d675aecabd5a62ae13184494a4b9db122ad24b3132e1d0e908eef802382f36",
"tmp/hub-core/hub_core/schemas/domain.py": "sha256:872a704fea959d2e4d0ff771770a97160457282218f4748ba4f830c489543aac",
"tmp/hub-core/hub_core/schemas/managed_repo.py": "sha256:a7ab3b1ab399da81324693dcd5d3feeccb211642b10f11cd3f03c7fe6ba751d4",
"tmp/hub-core/hub_core/schemas/policy.py": "sha256:bdaef1640783ac325a050cadf214e731d7c8e5e3a35dbb8f7562d5abbd209a4c",
"tmp/hub-core/hub_core/schemas/progress_event.py": "sha256:9ac7b7fd4881d424bfe59c5001554fd7302243e8489d5e777584d11ead1db328",
"tmp/hub-core/hub_core/schemas/tpsc.py": "sha256:5640d96a7fa862a468eeb0d6da002697b1f58e4cd675a4603b91e448d4db851e",
"tmp/hub-core/hub_core/security/__init__.py": "sha256:2754e25ec0f5725c33cb0b4dff1b7d10b364ebd828e609bb5b44eee3309d2d60",
"tmp/hub-core/hub_core/security/account_reader.py": "sha256:27019616b84cf030969a8e3c0850b5ae52f336008fd9daa4c26360566e74a39b",
"tmp/hub-core/hub_core/security/audit.py": "sha256:94cb85d71f065443c6423871b59d41e929af0441c95928273caf3e38dad8125c",
"tmp/hub-core/hub_core/security/boundary.py": "sha256:a1629ac717672276f61015c83f7af6e375bbd8c1234c712dd3d225da307ae0d6",
"tmp/hub-core/hub_core/security/browser.py": "sha256:fd474ee4b9611fe8dd2c60111402c2fce67d680a7da3519b5d4a495a6b559b7a",
"tmp/hub-core/hub_core/security/config.py": "sha256:7d604f6aeebde393a371ec3e0ab174934e10808c29fb370acba031486e9c02a3",
"tmp/hub-core/hub_core/security/context.py": "sha256:1181bbf12ba86bfd5046dab2f3a22a64ee3686be6dd52ab3e843303349b13a52",
"tmp/hub-core/hub_core/security/facts.py": "sha256:3de4db38ffd3f86f4918d76f811289cd501be3c28dcba16f5088aa2a2b7529ce",
"tmp/hub-core/hub_core/security/identity.py": "sha256:d7a0bab9d59a5f342fdedc9723d818a9d2537a59c6a9415455786af748f912e9",
"tmp/hub-core/hub_core/security/policy.py": "sha256:a2370e9ea5b04b1d5a429644d7f81edb1cbe8e15214c23b16cd6f949613eebd6",
"tmp/hub-core/hub_core/utils/__init__.py": "sha256:ce4ddb30ccc4fd2c4ee59e8a3c088a12165171af02aea957f17f72cd868e1899",
"tmp/hub-core/hub_core/utils/pagination.py": "sha256:366250b85052c7aabc2a2262521d34233124ba3a41026042792f8eda706ecb4f",
"tmp/hub-core/hub_core/utils/paths.py": "sha256:988a889be74f0626ca3c27b1d6677bd4a556e35de389c060d07ab64fa1c22fc8",
"tmp/hub-core/hub_core/utils/routing.py": "sha256:c94b44c5f1ab723e123503e20e0309f28abeeabea6d51beda79247d99852ad1e",
"tmp/hub-core/hub_core/utils/slugs.py": "sha256:eda7a7d18ff21a4d949223b75a2b615a949e615f461152a9cb9a2cf74b491e2d",
"app/.venv/lib/python3.12/site-packages/hub_core/__init__.py": "sha256:098578bae76f7d97354b8f3dfe8ddc63b15806a46d1691d70ed12d9839d69aec",
"app/.venv/lib/python3.12/site-packages/hub_core/conformance/__init__.py": "sha256:f7ce0a651913f157e586afd6e953b483add0dd5257ee9f2406c47e8d46ee9587",
"app/.venv/lib/python3.12/site-packages/hub_core/conformance/harness.py": "sha256:ca870fbbec7b7f5db6521d615fd76dfcbb9e97cbdfb037ffa993851a663b3c40",
"app/.venv/lib/python3.12/site-packages/hub_core/contracts/__init__.py": "sha256:ffb7a6b79ce15ddd57943bfbc40799e078c6f57c6690018237baa3f3a755ac49",
"app/.venv/lib/python3.12/site-packages/hub_core/contracts/helixforge_hub_extension/__init__.py": "sha256:32c095a16f777127ec0d2cd5e8373889866b81e9955d01e5adaf9503f6864da4",
"app/.venv/lib/python3.12/site-packages/hub_core/contracts/helixforge_hub_extension/v0_1_0/__init__.py": "sha256:e176b86b24d79c11f78ad714878bc3fca41c7821654fa766278b697360a41201",
"app/.venv/lib/python3.12/site-packages/hub_core/contracts/repository_navigation/__init__.py": "sha256:64912fe6673b72c3c0d8a2da28673d58bdf6f4185a1376ffe43d3735350adb60",
"app/.venv/lib/python3.12/site-packages/hub_core/contracts/repository_navigation/v1_0_0/__init__.py": "sha256:574cf1fee8433e0b11d9475cfbcc3837730b55f6ea1faa4920cba106d959464f",
"app/.venv/lib/python3.12/site-packages/hub_core/contracts/workload_projection/__init__.py": "sha256:c01198d23571442c1c3a596c7cd16be17dc1a3efdcfe3e8f9725b7dba16fc103",
"app/.venv/lib/python3.12/site-packages/hub_core/contracts/workload_projection/v1_0_0/__init__.py": "sha256:a9ecfecbf44893d32268288c872cdede0440d2a1373d37a47f69ba8c6431de0d",
"app/.venv/lib/python3.12/site-packages/hub_core/database.py": "sha256:a7704e97509433e90655b0ea22d5fa626d75b8b6470015ef8b9e5997e7a7f5ea",
"app/.venv/lib/python3.12/site-packages/hub_core/events.py": "sha256:c7125a242ed638a3ede29129e4ad826cd188cf76239b59a8587a40d182bbd0e7",
"app/.venv/lib/python3.12/site-packages/hub_core/mcp/__init__.py": "sha256:f97b25193573e56886b66f91cb8c86ef4a39f2100d709869e279e17456959859",
"app/.venv/lib/python3.12/site-packages/hub_core/mcp/credentials.py": "sha256:f5433d29848bd5a399a5e31695e0d908748e0a0856d26b2d691b667c50c95c34",
"app/.venv/lib/python3.12/site-packages/hub_core/mcp/server.py": "sha256:fc6a6de0a29ad9b6da176302a7d24e4ba987f667b6a7fb001029f00cb0d0c582",
"app/.venv/lib/python3.12/site-packages/hub_core/message_identity.py": "sha256:0ab996ba50f236c1342ce72ca8ae6ff28102ef41216972a745f7260e0c93fb24",
"app/.venv/lib/python3.12/site-packages/hub_core/migrations/__init__.py": "sha256:ccf4820e270c97fd5d4ac02f0c96362a772b8f4d1bcccc3f5e0077b77f575d36",
"app/.venv/lib/python3.12/site-packages/hub_core/migrations/env.py": "sha256:282e6e1a9e4a3cfdc111887f04dc5c1645307b3943a421852abe1709ce712e57",
"app/.venv/lib/python3.12/site-packages/hub_core/migrations/roles.py": "sha256:91e85297d296b0c4b9c78b16ae67d3a4707232d8f32b2cac2b2e84f31fa80e89",
"app/.venv/lib/python3.12/site-packages/hub_core/migrations/versions/0001_core_schema.py": "sha256:c65f076a7b56fb93ee67869e04d922ea396eab72a4c2d2f0dabd706c918aa081",
"app/.venv/lib/python3.12/site-packages/hub_core/migrations/versions/0002_runtime_ports.py": "sha256:d3ce15463841b3c13eaafdd137a65d4cf2839c0b8e8fddcb2cb2f1adbceb9f00",
"app/.venv/lib/python3.12/site-packages/hub_core/migrations/versions/0003_repository_navigation.py": "sha256:fb2d2cb5ed10f1760ab789ced8d43c657c1bedc29ba666231a70a18636160902",
"app/.venv/lib/python3.12/site-packages/hub_core/migrations/versions/0004_workload_projection.py": "sha256:87ce668bfef5cf4aaf0cfd3b48d498f2370f68e06a6e66375e446af6fbbb9c96",
"app/.venv/lib/python3.12/site-packages/hub_core/migrations/versions/0005_message_identity_aliases.py": "sha256:ec05d2c3cb906b78cdec4d220a7ce09264c38904c792cdad3cd1082e7eace7c0",
"app/.venv/lib/python3.12/site-packages/hub_core/migrations/versions/0006_outcome_outbox.py": "sha256:05effdf6f258836d1cd223f9d9fc3447d264484021d3a51cfa42517453d613e1",
"app/.venv/lib/python3.12/site-packages/hub_core/migrations/versions/__init__.py": "sha256:569f6d7e5cc1c17b7b1eee3eec3b82774b745d01ee1dfdf851a5c6f5c337f0bb",
"app/.venv/lib/python3.12/site-packages/hub_core/models/__init__.py": "sha256:c0b6de4986614a163dbb2467d15882eadbd66800478a50de4d6404198d1f773a",
"app/.venv/lib/python3.12/site-packages/hub_core/models/agent_message.py": "sha256:23d16eeaffe780f326b08e6c441100e6ae7b4075bbb6421734c06156a87c4375",
"app/.venv/lib/python3.12/site-packages/hub_core/models/base.py": "sha256:85bb40bde6f4cf8e6c5700a3a016a711fab4adff77c5eb78f89827fe28b9537b",
"app/.venv/lib/python3.12/site-packages/hub_core/models/capability_catalog.py": "sha256:eebbb31f264821a61662d3c9e8c7eba74f46d7f68b946e35f8127343bf2ff9c4",
"app/.venv/lib/python3.12/site-packages/hub_core/models/capability_request.py": "sha256:70a867c82aaa061a4e1c671b023d213032464e776226280f99e2ca192e2a6eba",
"app/.venv/lib/python3.12/site-packages/hub_core/models/domain.py": "sha256:ddf45c37e3c37014ddd9148be01cf104658e1601abba2a8ba96f7680cdb581b3",
"app/.venv/lib/python3.12/site-packages/hub_core/models/managed_repo.py": "sha256:835c5d44581daf7d7dd95325e3f7fdf0add4d529b83779ed83b5e812dcb3aa61",
"app/.venv/lib/python3.12/site-packages/hub_core/models/message_identity_alias.py": "sha256:d07d59cd9c9351669e8319727d08bc0e9bb7e0ee36bf8a34c0c258f0b4ea61e4",
"app/.venv/lib/python3.12/site-packages/hub_core/models/progress_event.py": "sha256:ee750127db2f49c5b9b94773a408f7f965d834b02e6d962220f4bc762309161f",
"app/.venv/lib/python3.12/site-packages/hub_core/models/tpsc.py": "sha256:cb28cbe3512486adb73c0bce633b3c542a41a64e7ec673e86a5a25ab20d94afe",
"app/.venv/lib/python3.12/site-packages/hub_core/routers/__init__.py": "sha256:800d1c45f597fcb4970ca7a83acd2b36016006efbdf638e14a3482a954d9046d",
"app/.venv/lib/python3.12/site-packages/hub_core/routers/capabilities.py": "sha256:18a403ad6ac961bc490543b698e079336f60ef9902a6cd71ef9899076660d5c0",
"app/.venv/lib/python3.12/site-packages/hub_core/routers/domains.py": "sha256:d4c4061d2ab0d2d6390a10f4f815deb0b93b3979345aee965b666110ba32e6e0",
"app/.venv/lib/python3.12/site-packages/hub_core/routers/messages.py": "sha256:903bd4f8c8ca599b606cf2229315de21097360816fcda907159dac4f48cee512",
"app/.venv/lib/python3.12/site-packages/hub_core/routers/policy.py": "sha256:358271263295fdebb5bcb95c3a27bd3ea456964a77bc01c3f784fb7a8754aacf",
"app/.venv/lib/python3.12/site-packages/hub_core/routers/progress.py": "sha256:680cf4dea00a1734d7d70ef7880acc8fd39f045521baa47f415c17934779d18a",
"app/.venv/lib/python3.12/site-packages/hub_core/routers/repos.py": "sha256:1bca6c9b2b5bbe172f5b935b763ac1e399b1d403cdc2c463000ae79f843257a2",
"app/.venv/lib/python3.12/site-packages/hub_core/routers/tpsc.py": "sha256:706815b01be1c0320bf73cf9cff3e9415c1f27b8449ee9e6fea67b12d3042fd1",
"app/.venv/lib/python3.12/site-packages/hub_core/runtime/__init__.py": "sha256:c522e626373407bf9bcd727dac7bb812cbdc8c3ef5bb6509a74949e5b49ab4c2",
"app/.venv/lib/python3.12/site-packages/hub_core/runtime/app.py": "sha256:87b077498bbe321c73139e51f7ca689fdc7963a98c4d72a5a3bbcc564f47ee76",
"app/.venv/lib/python3.12/site-packages/hub_core/runtime/cli.py": "sha256:98ec00f2a12911245827f515877472ae1c37cea65a42454494450ffbe7380e57",
"app/.venv/lib/python3.12/site-packages/hub_core/runtime/compat.py": "sha256:9f1c4575bb406ae1174da1a005ec16f44ace63768617e11164b44b4051162d0d",
"app/.venv/lib/python3.12/site-packages/hub_core/runtime/compat_catalogs.py": "sha256:250f61ea39cced32c8cd932c4e770ae8be54ff1cf1e257ee2c82324bde0d7a26",
"app/.venv/lib/python3.12/site-packages/hub_core/runtime/config.py": "sha256:009b2fc5d1f8fc390d478b01444f610ada91e5fee30534936382be2529a843fc",
"app/.venv/lib/python3.12/site-packages/hub_core/runtime/inbox_projection.py": "sha256:84499238654310b86f7a97f2817163ff4c4da0dfce562c677294fa3dfef7f398",
"app/.venv/lib/python3.12/site-packages/hub_core/runtime/migration.py": "sha256:d12731fa581c48bf58deec19405ece977c89d08993b2a2ad047cb1fd17318ea8",
"app/.venv/lib/python3.12/site-packages/hub_core/runtime/models.py": "sha256:367ca4aa29f59f8e3f59eab22f12782146cf623fa7ac7c22346c794b85a88185",
"app/.venv/lib/python3.12/site-packages/hub_core/runtime/ports.py": "sha256:ab70a401deae73389761a0e88dc8ee843df00a671fae6919b1f7eed344cfb542",
"app/.venv/lib/python3.12/site-packages/hub_core/runtime/postgres_store.py": "sha256:9286e7845009b66f00069d67177733dabe372d79986c33ca9b43e00dfa5a2341",
"app/.venv/lib/python3.12/site-packages/hub_core/runtime/repo_manager_client.py": "sha256:8fa305277c5f3ddfdbe1daf4023ce6c3cde03b5c18c1292dd6aac3fe995aabe1",
"app/.venv/lib/python3.12/site-packages/hub_core/runtime/repository_navigation.py": "sha256:f3503596781f3e7f7d53dd28dd3aaf563905f1398575b40c8080e2b1b9978b1e",
"app/.venv/lib/python3.12/site-packages/hub_core/runtime/repository_navigation_routes.py": "sha256:eab78ab418a7b542dcdcab5da023bacc65fb50520853f8d179accb2105e373cf",
"app/.venv/lib/python3.12/site-packages/hub_core/runtime/store.py": "sha256:2fb78f14bab8df08807322d6f6416745856d69e6001381246abbac879891858d",
"app/.venv/lib/python3.12/site-packages/hub_core/runtime/tables.py": "sha256:a5ff72a9c54eedcc8bb81330e62ee72578a383a1d03a1acb598c72c96cdfbe9e",
"app/.venv/lib/python3.12/site-packages/hub_core/runtime/validation.py": "sha256:2add804d95d5d0469d6d8d605fbd1013885da972489a017260c6fddef7b80c79",
"app/.venv/lib/python3.12/site-packages/hub_core/runtime/workload_projection.py": "sha256:8500260404b0c566a62e51fef63ee320d4e3dafe5e2467f44db884296b9e3bf4",
"app/.venv/lib/python3.12/site-packages/hub_core/runtime/workload_projection_routes.py": "sha256:5c71189b98867f2f1f7612f47ee69c0ec3ddf836b9b1c7e134b98a2be45791de",
"app/.venv/lib/python3.12/site-packages/hub_core/schemas/__init__.py": "sha256:dfa2ec2d125f03a532637d2fbef3058ebe1f85c515ff42a930198e164b7812f7",
"app/.venv/lib/python3.12/site-packages/hub_core/schemas/agent_message.py": "sha256:d6a7f40dc9a3c61be761f4d2e2c08f726fb0a423ef73e273a7cd97c69ae6c6a3",
"app/.venv/lib/python3.12/site-packages/hub_core/schemas/capability.py": "sha256:949c55cb1f2694de4617ad53dc9cb7bc0e72addd7b9d0f104b93d783e4edc647",
"app/.venv/lib/python3.12/site-packages/hub_core/schemas/doi.py": "sha256:73d675aecabd5a62ae13184494a4b9db122ad24b3132e1d0e908eef802382f36",
"app/.venv/lib/python3.12/site-packages/hub_core/schemas/domain.py": "sha256:872a704fea959d2e4d0ff771770a97160457282218f4748ba4f830c489543aac",
"app/.venv/lib/python3.12/site-packages/hub_core/schemas/managed_repo.py": "sha256:a7ab3b1ab399da81324693dcd5d3feeccb211642b10f11cd3f03c7fe6ba751d4",
"app/.venv/lib/python3.12/site-packages/hub_core/schemas/policy.py": "sha256:bdaef1640783ac325a050cadf214e731d7c8e5e3a35dbb8f7562d5abbd209a4c",
"app/.venv/lib/python3.12/site-packages/hub_core/schemas/progress_event.py": "sha256:9ac7b7fd4881d424bfe59c5001554fd7302243e8489d5e777584d11ead1db328",
"app/.venv/lib/python3.12/site-packages/hub_core/schemas/tpsc.py": "sha256:5640d96a7fa862a468eeb0d6da002697b1f58e4cd675a4603b91e448d4db851e",
"app/.venv/lib/python3.12/site-packages/hub_core/security/__init__.py": "sha256:2754e25ec0f5725c33cb0b4dff1b7d10b364ebd828e609bb5b44eee3309d2d60",
"app/.venv/lib/python3.12/site-packages/hub_core/security/account_reader.py": "sha256:27019616b84cf030969a8e3c0850b5ae52f336008fd9daa4c26360566e74a39b",
"app/.venv/lib/python3.12/site-packages/hub_core/security/audit.py": "sha256:94cb85d71f065443c6423871b59d41e929af0441c95928273caf3e38dad8125c",
"app/.venv/lib/python3.12/site-packages/hub_core/security/boundary.py": "sha256:a1629ac717672276f61015c83f7af6e375bbd8c1234c712dd3d225da307ae0d6",
"app/.venv/lib/python3.12/site-packages/hub_core/security/browser.py": "sha256:fd474ee4b9611fe8dd2c60111402c2fce67d680a7da3519b5d4a495a6b559b7a",
"app/.venv/lib/python3.12/site-packages/hub_core/security/config.py": "sha256:7d604f6aeebde393a371ec3e0ab174934e10808c29fb370acba031486e9c02a3",
"app/.venv/lib/python3.12/site-packages/hub_core/security/context.py": "sha256:1181bbf12ba86bfd5046dab2f3a22a64ee3686be6dd52ab3e843303349b13a52",
"app/.venv/lib/python3.12/site-packages/hub_core/security/facts.py": "sha256:3de4db38ffd3f86f4918d76f811289cd501be3c28dcba16f5088aa2a2b7529ce",
"app/.venv/lib/python3.12/site-packages/hub_core/security/identity.py": "sha256:d7a0bab9d59a5f342fdedc9723d818a9d2537a59c6a9415455786af748f912e9",
"app/.venv/lib/python3.12/site-packages/hub_core/security/policy.py": "sha256:a2370e9ea5b04b1d5a429644d7f81edb1cbe8e15214c23b16cd6f949613eebd6",
"app/.venv/lib/python3.12/site-packages/hub_core/utils/__init__.py": "sha256:ce4ddb30ccc4fd2c4ee59e8a3c088a12165171af02aea957f17f72cd868e1899",
"app/.venv/lib/python3.12/site-packages/hub_core/utils/pagination.py": "sha256:366250b85052c7aabc2a2262521d34233124ba3a41026042792f8eda706ecb4f",
"app/.venv/lib/python3.12/site-packages/hub_core/utils/paths.py": "sha256:988a889be74f0626ca3c27b1d6677bd4a556e35de389c060d07ab64fa1c22fc8",
"app/.venv/lib/python3.12/site-packages/hub_core/utils/routing.py": "sha256:c94b44c5f1ab723e123503e20e0309f28abeeabea6d51beda79247d99852ad1e",
"app/.venv/lib/python3.12/site-packages/hub_core/utils/slugs.py": "sha256:eda7a7d18ff21a4d949223b75a2b615a949e615f461152a9cb9a2cf74b491e2d",
"app/api/routers/workstreams.py": "sha256:9c9bf4376dd323b52109912bb3f615949e89ac02c8b88b4720f569f8565ad1d6",
"app/api/services/forge_projection.py": "sha256:b980e62cbd85c3e7df42b01abf14354e04133374db94af6531dcd91629c3487d",
"app/api/services/historical_projection.py": "sha256:e24e57c58f4aaa4ec9d9bf088009ac69006e53f75b9477ee53dc26c4e850b975",
"app/migrations/versions/c4d5e6f7a8b9_inbox_feed_revision.py": "sha256:48f1730037be99b5623e878d7f20a06bec243ce27317fde81c82e16006d7b3ce",
"app/scripts/consistency_check.py": "sha256:eaee916f46a11f49d2621fc9386717d73b7fd517acbe4b695e7b551d67bfa8f7"
},
"layers": [
{
"digest": "sha256:304074f06047f3dee0c586de265e0e27b577930bc80dc7f97f3829b188bc4fb0",
"compressed_bytes": 30855435,
"archive_entries": 3271,
"whiteouts": 0
},
{
"digest": "sha256:75b6a36c64a1cf3520fc4b4aae4739d5caa8a6a05c27835a920a61784c9c32f8",
"compressed_bytes": 1294052,
"archive_entries": 553,
"whiteouts": 0
},
{
"digest": "sha256:6912e23eb44595b44dc6d9882b34f305ad6f6e9667cec70b13e5dbc4f8cd97f4",
"compressed_bytes": 12121637,
"archive_entries": 1828,
"whiteouts": 0
},
{
"digest": "sha256:bbc5a12237474ec0b23b5051260d56d6436a24d70d9cad961adebe7a037ed813",
"compressed_bytes": 249,
"archive_entries": 8,
"whiteouts": 0
},
{
"digest": "sha256:e16e3995c15e64ae03e700d74eab2389f1151e6bd5d40f55faa047e9124aeb20",
"compressed_bytes": 93,
"archive_entries": 1,
"whiteouts": 0
},
{
"digest": "sha256:14568be832d0c877348328755de4e94cf4d0397d03f2161f6995e3b3e756e802",
"compressed_bytes": 55305356,
"archive_entries": 2898,
"whiteouts": 62
},
{
"digest": "sha256:2a6eb309967b62bf0c483ef7ca6b595a3222af9824d7d76df93588d2db516195",
"compressed_bytes": 148,
"archive_entries": 2,
"whiteouts": 0
},
{
"digest": "sha256:fb5adb1b205f7c2a5a53dc53c300aa7fa968e3bf2ba85bfcf569b8f45131ae94",
"compressed_bytes": 940,
"archive_entries": 2,
"whiteouts": 0
},
{
"digest": "sha256:2971f0a710787419ec533843974be96de47b083059b02e4a0dfdd48a98fdb458",
"compressed_bytes": 35700592,
"archive_entries": 5471,
"whiteouts": 0
},
{
"digest": "sha256:ab3e8c3939ccb8f5d731704f1e752ddb7fa9d135402990246724232683b00ff0",
"compressed_bytes": 598,
"archive_entries": 3,
"whiteouts": 0
},
{
"digest": "sha256:d1dbbe039887ff15d19e26f1fb3c559ea123a580c77bff5e286de1d6e3540595",
"compressed_bytes": 102610,
"archive_entries": 143,
"whiteouts": 0
},
{
"digest": "sha256:b62bd61a70f8477e54c01355ae337a553326e09974eebd65870d66f0b2f1ae3f",
"compressed_bytes": 109565,
"archive_entries": 160,
"whiteouts": 0
},
{
"digest": "sha256:99e6faeb00ebe8336cefd27da2bbedb3b4edc9d3c9e36ce05097163ba105a036",
"compressed_bytes": 475,
"archive_entries": 2,
"whiteouts": 0
},
{
"digest": "sha256:f910ccb3f35e942317fb817caf1ea13ddd0a49263e7bfbf6b8be6d132a663b29",
"compressed_bytes": 243471,
"archive_entries": 181,
"whiteouts": 0
},
{
"digest": "sha256:793e1ade12c8a9541429984963ef1084a96d41a69cbd5669d655829982b3bea7",
"compressed_bytes": 1675,
"archive_entries": 7,
"whiteouts": 0
},
{
"digest": "sha256:7a36c195ce44bc1ce1c7964cfd1fa3f3c32daf3dc9568a4ff7b550b0d93ef0fc",
"compressed_bytes": 36056,
"archive_entries": 7,
"whiteouts": 0
},
{
"digest": "sha256:231fbd5b3576f8c671a67522bad6a83935990a9a7fbe120ff26b422f78bd5f60",
"compressed_bytes": 33942,
"archive_entries": 63,
"whiteouts": 0
},
{
"digest": "sha256:23a0876f55835a5ee79ed45c5a67ba7ac9ca1e4e3168dfa2436dd234a370434a",
"compressed_bytes": 10801,
"archive_entries": 8,
"whiteouts": 0
},
{
"digest": "sha256:9a13796803f4255f7c8a1df9b693923fa002647c2690a148ebf901e4de66cb4f",
"compressed_bytes": 2116,
"archive_entries": 3,
"whiteouts": 0
},
{
"digest": "sha256:48d53f6a4a6c1a1f0ee5b6f45e8e8b47792ff9a4416c00c9c92454634155f915",
"compressed_bytes": 155200,
"archive_entries": 67,
"whiteouts": 0
},
{
"digest": "sha256:d5d5b11d971cbb2269dc1684898da2c0a41bff26630affbd8123ba7c82bd41b1",
"compressed_bytes": 2963,
"archive_entries": 7,
"whiteouts": 0
},
{
"digest": "sha256:d8438040b445dc9c2ad739b44d49d347d4c276ba889d5cdd8153fdadbe14f34d",
"compressed_bytes": 538,
"archive_entries": 4,
"whiteouts": 0
}
],
"python_package_metadata": {
"usr/local/lib/python3.12/site-packages/pip-25.0.1.dist-info/METADATA": {
"name": "pip",
"version": "25.0.1",
"metadata_sha256": "sha256:4fa7318cf30f979db7cec45c12cbbc2b4f88a15f12eefbf59a6291d0a03af926"
},
"usr/local/lib/python3.12/site-packages/uv-0.13.1.dist-info/METADATA": {
"name": "uv",
"version": "0.13.1",
"metadata_sha256": "sha256:3554d26fcb1db453b54bde82aa3830ddc37179f3ac4394d670361553c02d0cf2"
},
"app/.venv/lib/python3.12/site-packages/aiofile-3.12.3.dist-info/METADATA": {
"name": "aiofile",
"version": "3.12.3",
"metadata_sha256": "sha256:faaa3115c495ac1989aa6bda262b79a6c97e056c83e04afcfc71584a59e3ceb1"
},
"app/.venv/lib/python3.12/site-packages/alembic-1.20.0.dist-info/METADATA": {
"name": "alembic",
"version": "1.20.0",
"metadata_sha256": "sha256:bd32c118c43955c106bfa4255b2fb5ac9db410b0f7255a4cafb14b9e221ff41c"
},
"app/.venv/lib/python3.12/site-packages/annotated_doc-0.0.5.dist-info/METADATA": {
"name": "annotated-doc",
"version": "0.0.5",
"metadata_sha256": "sha256:8c24d70c5d9153d22123353a906042fd7603df2d15c2196290b0f8a962558c46"
},
"app/.venv/lib/python3.12/site-packages/annotated_types-0.8.0.dist-info/METADATA": {
"name": "annotated-types",
"version": "0.8.0",
"metadata_sha256": "sha256:614985b278f601b8ef863f82783b991cc3ada4b89f592eb71c7c48c5f2ef2298"
},
"app/.venv/lib/python3.12/site-packages/anyio-4.15.1.dist-info/METADATA": {
"name": "anyio",
"version": "4.15.1",
"metadata_sha256": "sha256:043aecb571af7189e8a6c528f5d8e32545f1b2cdfaaf761d51e6d5b67ea3ca81"
},
"app/.venv/lib/python3.12/site-packages/asyncpg-0.32.0.dist-info/METADATA": {
"name": "asyncpg",
"version": "0.32.0",
"metadata_sha256": "sha256:10c1e84f762a55c69630704e8a429793dcab1ce5edba7deadb35f40476d502c9"
},
"app/.venv/lib/python3.12/site-packages/attrs-26.1.0.dist-info/METADATA": {
"name": "attrs",
"version": "26.1.0",
"metadata_sha256": "sha256:4cd40e690f23bf37cbcad34efd6758e062df1df07c86833f7e3ce95bf1fa38cc"
},
"app/.venv/lib/python3.12/site-packages/authlib-1.9.1.dist-info/METADATA": {
"name": "Authlib",
"version": "1.9.1",
"metadata_sha256": "sha256:7141e57ccac936724fd004b401d0695e0ae48f32f2aee14d90259a9d4bc4fdbd"
},
"app/.venv/lib/python3.12/site-packages/beartype-0.23.1.dist-info/METADATA": {
"name": "beartype",
"version": "0.23.1",
"metadata_sha256": "sha256:ac75ec7a0bd6407db75c5c1da887798dfd4d5d12099e299602d03baa0ba22986"
},
"app/.venv/lib/python3.12/site-packages/cachetools-7.2.2.dist-info/METADATA": {
"name": "cachetools",
"version": "7.2.2",
"metadata_sha256": "sha256:76bbee4a06789a8c42253966988a07aff8f2f29cb1ae3ff9d6f16d661f01f2d4"
},
"app/.venv/lib/python3.12/site-packages/caio-0.12.9.dist-info/METADATA": {
"name": "caio",
"version": "0.12.9",
"metadata_sha256": "sha256:3128051d993cbf71f589f68b0082167fe1c910b17379d27653b6945ec848c3c8"
},
"app/.venv/lib/python3.12/site-packages/certifi-2026.7.22.dist-info/METADATA": {
"name": "certifi",
"version": "2026.7.22",
"metadata_sha256": "sha256:ef5af1638fbb23676ac3c5777dfcfc2cd9c348fe4172ed5ba3d277655b248090"
},
"app/.venv/lib/python3.12/site-packages/cffi-2.1.1.dist-info/METADATA": {
"name": "cffi",
"version": "2.1.1",
"metadata_sha256": "sha256:558bafb9093a8e790175d9ecff9688382981d744877dad86fd6770db0f4dc918"
},
"app/.venv/lib/python3.12/site-packages/click-8.5.0.dist-info/METADATA": {
"name": "click",
"version": "8.5.0",
"metadata_sha256": "sha256:e87bce0bd194de70dfb8e708e0a6b0483009f25611804eaf87bfeb63e6c20501"
},
"app/.venv/lib/python3.12/site-packages/cryptography-50.0.2.dist-info/METADATA": {
"name": "cryptography",
"version": "50.0.2",
"metadata_sha256": "sha256:047cfc71226409de4318b360aef15c63a2adf609731e3dcf7fe932f14ea042ee"
},
"app/.venv/lib/python3.12/site-packages/cyclopts-5.2.0.dist-info/METADATA": {
"name": "cyclopts",
"version": "5.2.0",
"metadata_sha256": "sha256:a1bcd1e587744ec6f5c5c64126ac03a03fc2468f7c91cd1c44572a5ac11dcb2a"
},
"app/.venv/lib/python3.12/site-packages/dnspython-2.9.0.dist-info/METADATA": {
"name": "dnspython",
"version": "2.9.0",
"metadata_sha256": "sha256:d25dbd09b39758cd5cebe518701012a6278731986cd384fc4a7f25e78f7de929"
},
"app/.venv/lib/python3.12/site-packages/docstring_parser-0.18.0.dist-info/METADATA": {
"name": "docstring_parser",
"version": "0.18.0",
"metadata_sha256": "sha256:cbc42027997fd4b194ae5fd064a4d1e57b92976941d0c26d369561a7c016a333"
},
"app/.venv/lib/python3.12/site-packages/email_validator-2.3.0.dist-info/METADATA": {
"name": "email-validator",
"version": "2.3.0",
"metadata_sha256": "sha256:2a97b81eefcd856bc80b0346f47fa2d800b9a438bf8f9231ae00fe931d5c9fbc"
},
"app/.venv/lib/python3.12/site-packages/exceptiongroup-1.3.1.dist-info/METADATA": {
"name": "exceptiongroup",
"version": "1.3.1",
"metadata_sha256": "sha256:81984a523a2f7a522ad12bea784a8bb85edec0805ebbd0fb4e350169a36dd802"
},
"app/.venv/lib/python3.12/site-packages/fastapi-0.143.3.dist-info/METADATA": {
"name": "fastapi",
"version": "0.143.3",
"metadata_sha256": "sha256:364f68b16f489abe36d621da2fa727f774bec90916cd3325b78e422af3b59a5f"
},
"app/.venv/lib/python3.12/site-packages/fastmcp-4.1.0.dist-info/METADATA": {
"name": "fastmcp",
"version": "4.1.0",
"metadata_sha256": "sha256:cfdcfd24ad904514087ec1735baa54df174f72f570833a16b6074887cfc51fa6"
},
"app/.venv/lib/python3.12/site-packages/fastmcp_slim-4.1.0.dist-info/METADATA": {
"name": "fastmcp-slim",
"version": "4.1.0",
"metadata_sha256": "sha256:e8eca5fbae348ee6dcda0a0f1bf0dffa493e8d4d8763647ee48f4f871e35c998"
},
"app/.venv/lib/python3.12/site-packages/greenlet-3.5.6.dist-info/METADATA": {
"name": "greenlet",
"version": "3.5.6",
"metadata_sha256": "sha256:b9b4cbbc1f3165e50fbc4696c433fe82b8a151fde65eede28fb6ec5efb3e1fab"
},
"app/.venv/lib/python3.12/site-packages/griffelib-2.3.2.dist-info/METADATA": {
"name": "griffelib",
"version": "2.3.2",
"metadata_sha256": "sha256:fd24a68ed08309718d5759b8de3b90392b5804240d94834e6455be110ead34b3"
},
"app/.venv/lib/python3.12/site-packages/h11-0.16.0.dist-info/METADATA": {
"name": "h11",
"version": "0.16.0",
"metadata_sha256": "sha256:28f326098ac09fcba79b8f180f96087c841fe2456215cb7b872a9c7d5cd19d64"
},
"app/.venv/lib/python3.12/site-packages/httpcore-1.0.9.dist-info/METADATA": {
"name": "httpcore",
"version": "1.0.9",
"metadata_sha256": "sha256:fe2d4fda6199128978779e0cf27f3c045c531863fcdd987eacc74f3a18d41c21"
},
"app/.venv/lib/python3.12/site-packages/httpcore2-2.13.1.dist-info/METADATA": {
"name": "httpcore2",
"version": "2.13.1",
"metadata_sha256": "sha256:37a0ea0674e98b7c3a1f99c17d9bb5457de27a0f93c8b6560c9d17d525139abb"
},
"app/.venv/lib/python3.12/site-packages/httptools-0.9.0.dist-info/METADATA": {
"name": "httptools",
"version": "0.9.0",
"metadata_sha256": "sha256:8a743396f45465204e2b44b43cb57ec4a343c423f52604bbaf1777fd438698c9"
},
"app/.venv/lib/python3.12/site-packages/httpx-0.28.1.dist-info/METADATA": {
"name": "httpx",
"version": "0.28.1",
"metadata_sha256": "sha256:febb9b0f8f3e80d57c8199c304f35c4336e8581d1d18d7983c92766b82793b25"
},
"app/.venv/lib/python3.12/site-packages/httpx2-2.13.1.dist-info/METADATA": {
"name": "httpx2",
"version": "2.13.1",
"metadata_sha256": "sha256:b75b03a003b5593e000907cf7239fcc3f688b8f17db4ecff883d11221ea988fc"
},
"app/.venv/lib/python3.12/site-packages/idna-3.20.dist-info/METADATA": {
"name": "idna",
"version": "3.20",
"metadata_sha256": "sha256:dbd8c14c1e4ca1e0c9824a6dbc7cbbf78884f38eb52d91148cd3025f671e4b85"
},
"app/.venv/lib/python3.12/site-packages/jaraco.classes-3.4.0.dist-info/METADATA": {
"name": "jaraco.classes",
"version": "3.4.0",
"metadata_sha256": "sha256:2e6b102232edd45ae1bb8a6b4091fd40cf3201a6bb6fd4bc97c5e8cd765a44b8"
},
"app/.venv/lib/python3.12/site-packages/jaraco_context-6.1.2.dist-info/METADATA": {
"name": "jaraco.context",
"version": "6.1.2",
"metadata_sha256": "sha256:f1b65b60aea229af54e09b17ae0036cf048fe4a6161fa2e7966a8ea5c2321491"
},
"app/.venv/lib/python3.12/site-packages/jaraco_functools-4.6.0.dist-info/METADATA": {
"name": "jaraco.functools",
"version": "4.6.0",
"metadata_sha256": "sha256:6ed3cae5b8eef084c3c7b2385a0a8ac4c8155ab7c729212bea099bee69d5bd6c"
},
"app/.venv/lib/python3.12/site-packages/jeepney-0.9.0.dist-info/METADATA": {
"name": "jeepney",
"version": "0.9.0",
"metadata_sha256": "sha256:b8e6c353e9aaed0ed0144029556417fda23b64f344e718331907054ba05b8c62"
},
"app/.venv/lib/python3.12/site-packages/joserfc-1.7.5.dist-info/METADATA": {
"name": "joserfc",
"version": "1.7.5",
"metadata_sha256": "sha256:17df62bbdc7212bbcda3144e72720a62fc34bbf76c573062939120738545250f"
},
"app/.venv/lib/python3.12/site-packages/jsonref-1.1.0.dist-info/METADATA": {
"name": "jsonref",
"version": "1.1.0",
"metadata_sha256": "sha256:f98db3890394fba23912faab86ad8f70ed564fc8c885298b636b270f28453e43"
},
"app/.venv/lib/python3.12/site-packages/jsonschema-4.26.0.dist-info/METADATA": {
"name": "jsonschema",
"version": "4.26.0",
"metadata_sha256": "sha256:05b189866ca98a96da3934bf6b83687ced3ff85325273b77e16b705f6d8b26d2"
},
"app/.venv/lib/python3.12/site-packages/jsonschema_path-0.5.0.dist-info/METADATA": {
"name": "jsonschema-path",
"version": "0.5.0",
"metadata_sha256": "sha256:cd7e9a4426436980f099d826dbce8f61afa6603b496385fba9a9eb0ad59cdd48"
},
"app/.venv/lib/python3.12/site-packages/jsonschema_specifications-2025.9.1.dist-info/METADATA": {
"name": "jsonschema-specifications",
"version": "2025.9.1",
"metadata_sha256": "sha256:35abd41757f32b4ea24756920ded47b70173d77cbc052a5a6d3ab583b2e8d89d"
},
"app/.venv/lib/python3.12/site-packages/keyring-25.7.0.dist-info/METADATA": {
"name": "keyring",
"version": "25.7.0",
"metadata_sha256": "sha256:bd54de98fede6dc3e1f3cd89b4e37c95d8842a523bdbb9e52ab40edff1837163"
},
"app/.venv/lib/python3.12/site-packages/mako-1.4.3.dist-info/METADATA": {
"name": "Mako",
"version": "1.4.3",
"metadata_sha256": "sha256:39ea65f21e706265428ea6f2925520f419a10f0113a59318192ed38175d8838b"
},
"app/.venv/lib/python3.12/site-packages/markdown_it_py-4.2.0.dist-info/METADATA": {
"name": "markdown-it-py",
"version": "4.2.0",
"metadata_sha256": "sha256:72c36ff516755443b1ecb61e17b2567a990230b49a504b0eb0c42d5f82448a9c"
},
"app/.venv/lib/python3.12/site-packages/markupsafe-3.0.4.dist-info/METADATA": {
"name": "MarkupSafe",
"version": "3.0.4",
"metadata_sha256": "sha256:66767d76388ece15d49d50dafb655891ef5d868715e7128e7f7d737a70b353c4"
},
"app/.venv/lib/python3.12/site-packages/mcp-2.3.0.dist-info/METADATA": {
"name": "mcp",
"version": "2.3.0",
"metadata_sha256": "sha256:a94577513a4fa0cb639c279c1ad6f4d49650872d45f2f4ab9bb2d9c2a505d457"
},
"app/.venv/lib/python3.12/site-packages/mcp_types-2.3.0.dist-info/METADATA": {
"name": "mcp-types",
"version": "2.3.0",
"metadata_sha256": "sha256:eb973e8b7f53d52ab1df9d1f18854ffa84f074f6052e310089e2655961c48ca9"
},
"app/.venv/lib/python3.12/site-packages/mdurl-0.1.2.dist-info/METADATA": {
"name": "mdurl",
"version": "0.1.2",
"metadata_sha256": "sha256:b53b29d48f499367053fda3c81e7ce27d2558380ebbf83e66023b02eb7ddd25d"
},
"app/.venv/lib/python3.12/site-packages/more_itertools-11.2.1.dist-info/METADATA": {
"name": "more-itertools",
"version": "11.2.1",
"metadata_sha256": "sha256:8c558bfab61818450e78db4018281f8f7b22115e681cd53ae1d9d4931be54f15"
},
"app/.venv/lib/python3.12/site-packages/nats_py-2.16.0.dist-info/METADATA": {
"name": "nats-py",
"version": "2.16.0",
"metadata_sha256": "sha256:58ddf3dd698f9026d397e5bb56a0baac723ba221d3dddae0831cfacc75c34255"
},
"app/.venv/lib/python3.12/site-packages/openapi_pydantic-0.6.0.dist-info/METADATA": {
"name": "openapi-pydantic",
"version": "0.6.0",
"metadata_sha256": "sha256:e46a181161c62fa24d7d3d0ff2eaabea3a7b580ad7aa9f70d1f08bd129d46f49"
},
"app/.venv/lib/python3.12/site-packages/opentelemetry_api-1.45.1.dist-info/METADATA": {
"name": "opentelemetry-api",
"version": "1.45.1",
"metadata_sha256": "sha256:5bc71e90cb421be92a43bf5ad5b0f25dccfa2a24ddceaa2877779879962a4c9e"
},
"app/.venv/lib/python3.12/site-packages/packaging-26.3.dist-info/METADATA": {
"name": "packaging",
"version": "26.3",
"metadata_sha256": "sha256:70fdb89fc4d4a9a043bf7372b8972bcc883fddff34ab55e9cf80d73875384763"
},
"app/.venv/lib/python3.12/site-packages/pathable-0.6.0.dist-info/METADATA": {
"name": "pathable",
"version": "0.6.0",
"metadata_sha256": "sha256:c418727931012c2306cb8fa2726d044616c3178c1fc078ce182385dcf2b59922"
},
"app/.venv/lib/python3.12/site-packages/platformdirs-4.13.3.dist-info/METADATA": {
"name": "platformdirs",
"version": "4.13.3",
"metadata_sha256": "sha256:3814840758f662bb98053d08f99085fb77721812258573732b092fd7f2120c30"
},
"app/.venv/lib/python3.12/site-packages/psycopg2_binary-2.9.13.dist-info/METADATA": {
"name": "psycopg2-binary",
"version": "2.9.13",
"metadata_sha256": "sha256:a5267b4520191137e34589704162fce02cfd40efcfeb899c45187c7f6bfea7e5"
},
"app/.venv/lib/python3.12/site-packages/py_key_value_aio-0.4.6.dist-info/METADATA": {
"name": "py-key-value-aio",
"version": "0.4.6",
"metadata_sha256": "sha256:0c1af357b841bc3862e39543faa863346c487be9f2e2ffefba009e957a256bee"
},
"app/.venv/lib/python3.12/site-packages/pycparser-3.11.dist-info/METADATA": {
"name": "pycparser",
"version": "3.11",
"metadata_sha256": "sha256:e3a74c846648bb55f4f907284b745e2003b87b96d9c9d17a8a50cf37804541c9"
},
"app/.venv/lib/python3.12/site-packages/pydantic-2.14.1.dist-info/METADATA": {
"name": "pydantic",
"version": "2.14.1",
"metadata_sha256": "sha256:4d4c8e8d5e6a27d5208f0d7949f25628e6747133034969d4d1399b1ad5394036"
},
"app/.venv/lib/python3.12/site-packages/pydantic_core-2.50.1.dist-info/METADATA": {
"name": "pydantic_core",
"version": "2.50.1",
"metadata_sha256": "sha256:ae6817764b603fa2300cd31b9566ecc33476df46163c4150fffc353121a166f2"
},
"app/.venv/lib/python3.12/site-packages/pydantic_settings-2.16.0.dist-info/METADATA": {
"name": "pydantic-settings",
"version": "2.16.0",
"metadata_sha256": "sha256:470656112388bca95fd7d27b8954fda1f11dd6579563cfb9c0ea0445d1746b77"
},
"app/.venv/lib/python3.12/site-packages/pygments-2.21.0.dist-info/METADATA": {
"name": "Pygments",
"version": "2.21.0",
"metadata_sha256": "sha256:1dde075570136774c706bf0009183a793fe0ee262e4a5590cc6eff8453eedd43"
},
"app/.venv/lib/python3.12/site-packages/pyjwt-2.15.1.dist-info/METADATA": {
"name": "PyJWT",
"version": "2.15.1",
"metadata_sha256": "sha256:1ea0c9f139b8c928276b3628af165955f6f764491448a6ad8f103cdcbb382da3"
},
"app/.venv/lib/python3.12/site-packages/pyperclip-1.11.0.dist-info/METADATA": {
"name": "pyperclip",
"version": "1.11.0",
"metadata_sha256": "sha256:2493839e0751761f27d39aa6d5c5dfd1df839c45da8f328aba4ae4102868b825"
},
"app/.venv/lib/python3.12/site-packages/python_dotenv-1.2.4.dist-info/METADATA": {
"name": "python-dotenv",
"version": "1.2.4",
"metadata_sha256": "sha256:87cfee6042412d48df1100ea7822684988c3f2b79de7a250f4717f4df3026d6f"
},
"app/.venv/lib/python3.12/site-packages/python_multipart-0.0.32.dist-info/METADATA": {
"name": "python-multipart",
"version": "0.0.32",
"metadata_sha256": "sha256:e9d399d7dea5eddf0e4d24b5c24687a82e7e50bf2c2b30f8f8a06a64d887dd86"
},
"app/.venv/lib/python3.12/site-packages/pyyaml-6.0.3.dist-info/METADATA": {
"name": "PyYAML",
"version": "6.0.3",
"metadata_sha256": "sha256:03c3b415ed38d09faedc49360e930769b40c58f68585e6de00e2e4916a858d34"
},
"app/.venv/lib/python3.12/site-packages/referencing-0.37.0.dist-info/METADATA": {
"name": "referencing",
"version": "0.37.0",
"metadata_sha256": "sha256:c93b00e6ab1524fd60848376604c189a114374776475c25a31d57be47b29de6a"
},
"app/.venv/lib/python3.12/site-packages/rich-15.0.0.dist-info/METADATA": {
"name": "rich",
"version": "15.0.0",
"metadata_sha256": "sha256:fad34fc603fac4481bf81f8dd629492fc302c0ec31909c86644751512afb2e3b"
},
"app/.venv/lib/python3.12/site-packages/rich_rst-2.2.0.dist-info/METADATA": {
"name": "rich-rst",
"version": "2.2.0",
"metadata_sha256": "sha256:2cd1f39149e69bd94c4f1c30cb26c763e45689e6d6527b4c23afa1112c48f165"
},
"app/.venv/lib/python3.12/site-packages/rpds_py-2026.9.1.dist-info/METADATA": {
"name": "rpds-py",
"version": "2026.9.1",
"metadata_sha256": "sha256:5d72dca2e387c8d2b4180b68ee87c15b64751e059100a6db58891ec9995445c3"
},
"app/.venv/lib/python3.12/site-packages/secretstorage-3.5.0.dist-info/METADATA": {
"name": "SecretStorage",
"version": "3.5.0",
"metadata_sha256": "sha256:b98e3dc42ff7e9b1bacce60dac2662cd9965fad6e4469fc9483c9eb99998c367"
},
"app/.venv/lib/python3.12/site-packages/sqlalchemy-2.1.5.dist-info/METADATA": {
"name": "SQLAlchemy",
"version": "2.1.5",
"metadata_sha256": "sha256:7f568ea71aecfb29792ccee2c8b1345825e385d54b7e7bba755e445e53b5696f"
},
"app/.venv/lib/python3.12/site-packages/sse_starlette-3.5.0.dist-info/METADATA": {
"name": "sse-starlette",
"version": "3.5.0",
"metadata_sha256": "sha256:6dc401682bdae1a3e1f31b1c320b7f926c354643993840393875ebfc171c9014"
},
"app/.venv/lib/python3.12/site-packages/starlette-1.8.0.dist-info/METADATA": {
"name": "starlette",
"version": "1.8.0",
"metadata_sha256": "sha256:551e8d74d290002521c655b816b27654e49922b88cb474c8d710a893099036f8"
},
"app/.venv/lib/python3.12/site-packages/truststore-0.10.5.dist-info/METADATA": {
"name": "truststore",
"version": "0.10.5",
"metadata_sha256": "sha256:1fd496f290853214e630d96f7163d2dd916f84ebe634b58aa827d6853e8f8610"
},
"app/.venv/lib/python3.12/site-packages/typing_extensions-4.16.0.dist-info/METADATA": {
"name": "typing_extensions",
"version": "4.16.0",
"metadata_sha256": "sha256:b05084ca1d50879865178d9fff9fabeab61bdfb1f361bfbde95421ffc8f9be46"
},
"app/.venv/lib/python3.12/site-packages/typing_inspection-0.4.4.dist-info/METADATA": {
"name": "typing-inspection",
"version": "0.4.4",
"metadata_sha256": "sha256:bad3d9e6342ef6750cf5128ec942327b8bebdce0b6e4d6f67cb15b27ae5118da"
},
"app/.venv/lib/python3.12/site-packages/uncalled_for-0.4.1.dist-info/METADATA": {
"name": "uncalled-for",
"version": "0.4.1",
"metadata_sha256": "sha256:45a21666ddfa8ca8d4e2423c05edaeaa7827686a3ae4fed82b24427854745c2f"
},
"app/.venv/lib/python3.12/site-packages/uvicorn-0.54.0.dist-info/METADATA": {
"name": "uvicorn",
"version": "0.54.0",
"metadata_sha256": "sha256:9399e5ddda3d26e842d6fb5420c38b0857aac568d53badbdc224b3a21339efa8"
},
"app/.venv/lib/python3.12/site-packages/uvloop-0.23.0.dist-info/METADATA": {
"name": "uvloop",
"version": "0.23.0",
"metadata_sha256": "sha256:fe11a50198ffd4d9f1cb20922dc638ad459d5e63e6c615b0c1c69448d3362977"
},
"app/.venv/lib/python3.12/site-packages/watchfiles-1.3.0.dist-info/METADATA": {
"name": "watchfiles",
"version": "1.3.0",
"metadata_sha256": "sha256:1fba87d08d66f10f7515e4817abd4ac7f27abf3300cb33b7ad3f79762987a502"
},
"app/.venv/lib/python3.12/site-packages/websockets-17.2.dist-info/METADATA": {
"name": "websockets",
"version": "17.2",
"metadata_sha256": "sha256:0d5f1279d095e7ba84ef21614af0822dc500ccb2e51b7c3a19eda7159b2dade4"
},
"app/.venv/lib/python3.12/site-packages/hub_core-0.2.0.dist-info/METADATA": {
"name": "hub-core",
"version": "0.2.0",
"metadata_sha256": "sha256:84a59c6ea36d6d8a0b91b76e52f351a5ce6f2fd9ace8a4c887b51bab05973953"
}
},
"package_metadata_equal_to_deployed_image": true,
"hub_core_source_hashes_equal_to_deployed_image": true,
"image_executed": false,
"image_installed_or_daemon_pulled": false,
"anonymous_protocol_only": true,
"token_retained": false,
"limitations": [
"Selected code hashes and package inventory do not prove full application acceptance or hermetic provenance.",
"Floating dependency origins remain unknown; observed byte changes require explicit review.",
"New API behavior is not deployed or natively accepted."
]
}

View file

@ -0,0 +1,14 @@
{
"workplan_id": "dfadb19b-445e-59c6-bcd6-48e87b912c52",
"id": "da342cba-86c9-4aa6-9e15-c34bc7370be8",
"topic_id": null,
"task_id": null,
"decision_id": "df703266-6719-4c04-ae1b-ba9afd3bca1c",
"event_type": "note",
"summary": "Founder-approved frozen-binding API guard revision2 SHA5d0ddebc deployedexactc103cbf/4cc4 atHelm77. API/MCPReady,schemac4unchanged, eightactualnative intercepted/rollbackcasespasswithzero commits, all18CURRENT historicalfullrows preserved acrossrollout/probes/normalCluster20ordinarybindingpipelineandfinalallpublications. Threefrozenarchives andunrelatedstaged/dirtyworkpreserved. ExactownedJob99ebab/podobservedSucceededthenabsent; exactownedchartstagingremoved,norecovery. Originalthreeparentmismatch retainedasfounderapprovedhistoricalexception withoutguessedrestore/backfilledsuccess. Verifiedreceivingreturn09958bd6. SourceStateHub200cbd0a finishesSTATE-WP0095/all10done,declaredimagepinsconsistent; RepoManager3bcb1e1c finishesRMGR-WP0019/all6done. All32coordinator+affectedownertaskbindings/UUIDs verifiedsource/remote/primary. Custodiancheckpointceaeef1e committedthroughnormalhooksandnormalfix-consistency/rmgrsync appliedprimary expected=derived; rootconsistency0fail/0automationerror/7warnings. Existingbinding-guard drivercasesatisfied; TLS/storage/nativeconsumerwaitsremainlive. All16native targetgatesstayopen. ActiveoriginalgoalcontinueswithGPT6.1Solmediumaudit,storageassignment/nativeproposal,andidentityTLS/selectedconsumerpreparation; no newnativeidentity/storageauthorizationinferred.",
"detail": null,
"author": "codex",
"session_id": null,
"created_at": "2026-10-17T02:06:17.176566Z",
"workstream_id": "dfadb19b-445e-59c6-bcd6-48e87b912c52"
}

View file

@ -0,0 +1,152 @@
{
"credential_values_exposed": false,
"historical_module_sha256": "sha256:e24e57c58f4aaa4ec9d9bf088009ac69006e53f75b9477ee53dc26c4e850b975",
"raw_rows_retained": false,
"schema": "state95.full-db-row-hash-snapshot.v1",
"status": "verified-current-rows-after-all-publication",
"tables": {
"tasks": {
"all_rows_sha256": "sha256:7f42dea1cabd2f5802aab5e2b1535a530192a1f35721ca1ca2cddc2fb5319cc9",
"columns": [
"assignee",
"blocked_on",
"blocking_reason",
"created_at",
"decision_id",
"description",
"due_date",
"flavor",
"id",
"intervention_note",
"needs_human",
"parent_task_id",
"priority",
"record_id",
"status",
"title",
"updated_at",
"wait_until",
"workplan_id"
],
"row_count": 15,
"rows": [
{
"sha256": "sha256:89b3be69fbaf5ad60f5aefa6f2b630e1d6124e0fca7b01a5e9ce1ff41a451106",
"uuid": "05d42a55-921f-4aa7-bb76-e8af9c7e0ac3"
},
{
"sha256": "sha256:3aa20e9695e110468d13baa3c813ac5b474896d89eeb6edc56e9ea00d9199fd7",
"uuid": "08233868-d522-4117-bc4e-6c0f52545665"
},
{
"sha256": "sha256:72fb54d9a271067a62f33377d56e0bb9e6874a3f9c475b9ae049609b93d1f643",
"uuid": "140da396-8e30-4f4d-b88c-c42c0cd46c01"
},
{
"sha256": "sha256:15198017e5c7d31b4e8ce1d5c0ef0de99ac1c50d3bffd0ea0ee807bef70ce5d1",
"uuid": "2d5acff7-4a4e-4ddd-ad06-08237ad3dac8"
},
{
"sha256": "sha256:29a6f1e50db6a7b4ba5b98a2f7d8d109b4df1720fce58777c21f7538084f8cda",
"uuid": "3f042630-eab0-4c6a-9167-e2b28ff20e40"
},
{
"sha256": "sha256:2e00141f0d7382dd34859b686a500dc740a543efe81e23f5805754cbc6761f2e",
"uuid": "4526a842-ea31-4874-9231-92ab556cfe7b"
},
{
"sha256": "sha256:7b3ecacb7be3c1a24a100d7b732b7f60e1a99329a6847612264cd01d13c7ecf0",
"uuid": "5c3d40e4-239b-488e-9519-6f7a38d2325f"
},
{
"sha256": "sha256:71107455d1d453d69d65466ca66da1fd4ee4aa95ecdf7920b9acdce767f0c55c",
"uuid": "6841c93a-f146-47eb-9f7c-8fa0e02c1bbc"
},
{
"sha256": "sha256:f8e44ea79125870ab3ade44afab8752c126c424e2b751f48b9fe555e00b5b708",
"uuid": "7f9e0e58-a130-467a-a2d0-b3f2564e496f"
},
{
"sha256": "sha256:435e39ffee3a59eae3b890739a6ed8e06801fb23dead15073653d8650f9d5ff5",
"uuid": "a6313e06-1976-46a7-8e31-df4eb2eca880"
},
{
"sha256": "sha256:2fcda8440a166fc628f8fadf91a4a23ba7728ccac2e629248c6eccd00d5d4345",
"uuid": "ab166073-30a7-4702-a037-4091e8706e20"
},
{
"sha256": "sha256:b2e11b8fdc580e6cb7ec20c7e90018ba9fb468893b6eda5b9519dc68737a0ca0",
"uuid": "d8a3ba40-fda0-4c1f-a9f1-ffcd621a5b3d"
},
{
"sha256": "sha256:788862d9cc5015b927d27303585431a0db4af6077178ab3bfdde37f900cee821",
"uuid": "dab2c07f-8aa0-4635-8df6-857e87e93fc5"
},
{
"sha256": "sha256:f7bc1f3acf187301430df0623417fb90b6af1869c0383a0bff9ec25f79ffe4af",
"uuid": "e8510646-46ed-4697-a345-f3d3009eea78"
},
{
"sha256": "sha256:f2760deded2f7136a60fba0156c4956ba373fbcf5eda986f5c380d7ee9c925ff",
"uuid": "f8e4a094-c367-40eb-b895-da17bc144b07"
}
]
},
"workplans": {
"all_rows_sha256": "sha256:1cd4e695c8f8da8bdb2335a8770a219f704f3989f14d28ebf0b1494f2458dc4a",
"columns": [
"backing_archived",
"backing_filename",
"backing_relative_path",
"backing_synced_at",
"concurrency_mode",
"created_at",
"derived_from_commit",
"description",
"due_date",
"execution_group",
"execution_state",
"flavor",
"flavor_promoted_from",
"flavor_promotion_reason",
"id",
"launch_mode",
"owner",
"planning_order",
"planning_priority",
"projection_retired_at",
"projection_retired_reason",
"queue_rank",
"repo_goal_id",
"repo_id",
"scheduled_for",
"slug",
"status",
"title",
"topic_id",
"updated_at"
],
"row_count": 3,
"rows": [
{
"sha256": "sha256:bfd3b3abe48d989b1c253d8cca57c463c40e63413885f467b3f3e2187d33cb49",
"uuid": "4c63dfc6-9eac-4e79-9f77-8f644ad7147d"
},
{
"sha256": "sha256:c3f19622843b62cea0fd987ed550fe8d050214a952f77f9250e1696506130303",
"uuid": "7e8b0c20-51eb-40c9-9e3b-85dd380d7625"
},
{
"sha256": "sha256:a406d311c2c0421eb7646edf4d215dbd96f570d07b2ba0bc6a747d7c573f0b9a",
"uuid": "7ee9ee22-1fae-4567-9194-8d70a9e0f45b"
}
]
}
},
"helm_revision": 77,
"api_mcp_ready": true,
"exact_image": "forgejo.coulomb.social/coulomb/state-hub:main-c103cbf@sha256:4cc4ce17bbcdb1b66132d07c5b7b741f46df83124ef1c8403ffabdafba6993d1",
"checked_at": "2026-10-17T02:05:51.453898+00:00",
"original_failed_comparison_retained": true,
"original_preimages_restored": false
}

View file

@ -0,0 +1,12 @@
{
"status": "verified",
"commit": "ceaeef1ee79eeba37e7cacf73ba8aed3357a5e7c",
"remote_exact": true,
"primary_projection_expected_equals_derived": true,
"task_bindings": 32,
"state95_and_registrar19_finished": true,
"approved_revision": 2,
"runtime_return_and_proposal_bound_receipts_unchanged": true,
"unrelated_index_and_worktree_preserved": true,
"native_target_gates_closed": 0
}

View file

@ -0,0 +1,131 @@
{
"repo_slug": "the-custodian",
"repo_path": "/home/worsch/the-custodian",
"source_revision": "ceaeef1ee79eeba37e7cacf73ba8aed3357a5e7c",
"source_revision_reason": null,
"issues": [
{
"severity": "WARN",
"check_id": "C-35",
"message": "Repo Manager warning: workplans/: repository uses more than one workplan prefix: ['CUST-WP', 'THE-WP']",
"file_path": "workplans/",
"db_id": "",
"file_value": "",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-35",
"message": "Repo Manager contradictory: workplans/ADHOC-2026-08-25-retired.md: workplan id 'CUST-WP-ADHOC-2026-08-25-RETIRED' is not accepted by the canon kind registry",
"file_path": "workplans/ADHOC-2026-08-25-retired.md",
"db_id": "",
"file_value": "",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-05",
"message": "Title drift in 'cust-wp-0010': file='Domain and Repository Goals' db='Workplan Lifecycle Documentation' (file wins)",
"file_path": "workplans/CUST-WP-0010b-domain-and-repo-goals.md",
"db_id": "def8455a-9de4-4891-a15a-5b09ca98ab1e",
"file_value": "Domain and Repository Goals",
"db_value": "Workplan Lifecycle Documentation",
"fixable": true
},
{
"severity": "WARN",
"check_id": "C-20",
"message": "Workplan dependency target 'STATE-WP-0092' is not linked to State Hub",
"file_path": "workplans/CUST-WP-0072-fleet-flavor-and-depends-on-backfill.md",
"db_id": "",
"file_value": "STATE-WP-0092",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-40",
"message": "blocker satisfied, task still wait: 'CUST-WP-0073-T05' in workplan 'CUST-WP-0073' \u2014 decision 1591daa1-6e85-4dfd-aadd-5a9fe1ef47fa is resolved",
"file_path": "workplans/CUST-WP-0073-agent-credential-separation.md#CUST-WP-0073-T05",
"db_id": "",
"file_value": "1591daa1-6e85-4dfd-aadd-5a9fe1ef47fa",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-39",
"message": "Human wait task 'CUST-WP-0079-T06' in workplan 'CUST-WP-0079' has needs_human: true without decision_id \u2014 unrouted human gate",
"file_path": "workplans/CUST-WP-0079-deterministic-security-flow.md#CUST-WP-0079-T06",
"db_id": "",
"file_value": "needs_human: true",
"db_value": "",
"fixable": false
},
{
"severity": "INFO",
"check_id": "C-08",
"message": "Closed DB workstream 'cust-wp-0010b' (id=416ff571\u2026, status=finished) has no backing workplan file",
"file_path": "",
"db_id": "416ff571-18cf-5cb2-9c8b-836f68a301b3",
"file_value": "",
"db_value": "",
"fixable": false
},
{
"severity": "INFO",
"check_id": "C-08",
"message": "Retired DB workstream 'interactive-the-custodian@retired-20260828' (id=370c2481\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work",
"file_path": "",
"db_id": "370c2481-6806-41eb-a917-f8874f03184f",
"file_value": "",
"db_value": "",
"fixable": false
},
{
"severity": "INFO",
"check_id": "C-08",
"message": "Retired DB workstream 'the-custodian-adhoc-2026-07-02@retired-20260827' (id=5c53733b\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work",
"file_path": "",
"db_id": "5c53733b-c077-4a93-be66-9c3af8f9b6fd",
"file_value": "",
"db_value": "",
"fixable": false
},
{
"severity": "INFO",
"check_id": "C-08",
"message": "Retired DB workstream 'adhoc-2026-05-02@retired-20260827' (id=3f54ce9c\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work",
"file_path": "",
"db_id": "3f54ce9c-1f95-42de-894f-0f81a52ba2e8",
"file_value": "",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-26",
"message": "Workplan id 'THE-WP-0001' uses prefix 'THE-WP' but repo canonical prefix is 'CUST-WP' \u2014 new plans must conform",
"file_path": "workplans/THE-WP-0001-federation-interface.md",
"db_id": "",
"file_value": "THE-WP-0001",
"db_value": "CUST-WP",
"fixable": false
}
],
"fixes_applied": [
"bindings: synced 89/89",
"C-17 cleared: pushed 1 backlogged commit(s)",
"C-05 fixed: workstream def8455a\u2026 title \u2192 'Domain and Repository Goals'",
"push: pushed"
],
"summary": {
"fail": 0,
"automation_error": 0,
"warn": 7,
"info": 4
},
"result": "warn"
}

View file

@ -0,0 +1 @@
WARN: working-memory commit failed: Generated-only commits rejected: staged

View file

@ -0,0 +1,292 @@
{
"ok": true,
"status": "applied",
"repo_slug": "the-custodian",
"commit": "ceaeef1ee79eeba37e7cacf73ba8aed3357a5e7c",
"requests": 2,
"elapsed_ms": 9730.7,
"git": {
"upstream": "origin/main",
"behind": 0,
"ahead": 0
},
"identifiers": {
"schema": "repo-manager.identifier-assignment.v1",
"ok": true,
"executed": true,
"repo": "the-custodian",
"namespace": "helixforge",
"assignments": [],
"historical_invalid_identifiers": [
{
"kind": "workplan",
"id": "CUST-WP-ADHOC-2026-08-25-RETIRED",
"source": "workplans/ADHOC-2026-08-25-retired.md"
}
],
"historical_identity_collisions": [],
"files_changed": []
},
"receipt": {
"schema": "state-hub.repository-projection-reconcile.v1",
"instance_role": "primary",
"instance_label": "railiance01",
"expected_commit": "ceaeef1ee79eeba37e7cacf73ba8aed3357a5e7c",
"derived_commit": "ceaeef1ee79eeba37e7cacf73ba8aed3357a5e7c",
"outcome": {
"schema": "state-hub.projection-reset.v1",
"repo_slug": "the-custodian",
"commit": "ceaeef1ee79eeba37e7cacf73ba8aed3357a5e7c",
"status": "applied",
"counts": {
"created": 0,
"updated": 88,
"retired": 0,
"refused": 0,
"released": 0,
"created_tasks": 0,
"updated_tasks": 0,
"cancelled_tasks": 0,
"created_edges": 0,
"removed_edges": 0,
"unresolved_edges": 0,
"external_edges": 13
},
"created": [],
"updated": [
"CUST-WP-0000",
"CUST-WP-0000b",
"CUST-WP-0000c",
"CUST-WP-0001",
"CUST-WP-0002",
"CUST-WP-0004",
"CUST-WP-0005",
"CUST-WP-0006",
"CUST-WP-0007",
"CUST-WP-0008",
"CUST-WP-0009",
"CUST-WP-0010",
"CUST-WP-0010b",
"CUST-WP-0013",
"CUST-WP-0014",
"CUST-WP-0015",
"CUST-WP-0016",
"CUST-WP-0017",
"CUST-WP-0017-LEGACY",
"CUST-WP-0018",
"CUST-WP-0019",
"CUST-WP-0020",
"CUST-WP-0021",
"CUST-WP-0022",
"CUST-WP-0023",
"CUST-WP-0024",
"CUST-WP-0025",
"CUST-WP-0026",
"CUST-WP-0027",
"CUST-WP-0028",
"CUST-WP-0029",
"CUST-WP-0030",
"CUST-WP-0031",
"CUST-WP-0032",
"CUST-WP-0033",
"CUST-WP-0034",
"CUST-WP-0035",
"CUST-WP-0036",
"CUST-WP-0037",
"CUST-WP-0039",
"CUST-WP-0040",
"CUST-WP-0041",
"CUST-WP-0043",
"CUST-WP-0044",
"CUST-WP-0045",
"CUST-WP-0046",
"CUST-WP-0047",
"CUST-WP-0048",
"CUST-WP-0049",
"CUST-WP-0050",
"CUST-WP-0051",
"CUST-WP-0052",
"CUST-WP-0053",
"CUST-WP-0053-LEGACY",
"CUST-WP-0054",
"CUST-WP-0055",
"CUST-WP-0056",
"CUST-WP-0057",
"CUST-WP-0058",
"CUST-WP-0059",
"CUST-WP-0060",
"CUST-WP-0061",
"CUST-WP-0062",
"CUST-WP-0063",
"CUST-WP-0064",
"CUST-WP-0065",
"CUST-WP-0066",
"CUST-WP-0067",
"CUST-WP-0068",
"CUST-WP-0069",
"CUST-WP-0070",
"CUST-WP-0071",
"CUST-WP-0072",
"CUST-WP-0073",
"CUST-WP-0074",
"CUST-WP-0075",
"CUST-WP-0076",
"CUST-WP-0077",
"CUST-WP-0078",
"CUST-WP-0079",
"CUST-WP-0080",
"CUST-WP-0081",
"CUST-WP-ADHOC-2026-03-29",
"CUST-WP-ADHOC-2026-05-02",
"CUST-WP-ADHOC-2026-07-02",
"CUST-WP-ADHOC-2026-08-25",
"CUST-WP-ADHOC-2026-09-06",
"THE-WP-0001"
],
"retired": [],
"released": [],
"created_tasks": [],
"updated_tasks": [],
"cancelled_tasks": [],
"created_edges": [],
"removed_edges": [],
"unresolved_edges": [],
"external_edges": [
{
"workplan": "CUST-WP-0071",
"from": "CUST-WP-0071-T05",
"target": "CUST-WP-0071-T04",
"relationship_type": "starts_after",
"from_workplan_id": "2249bddb-7524-5add-bd5c-c4163a6ca0f3",
"from_task_id": "20ddd4d7-d85d-5b95-bb0a-7b0ca8af76ca",
"to_workplan_id": null,
"to_task_id": "944f98e2-7e42-5894-925b-752423f3a1cc"
},
{
"workplan": "CUST-WP-0071",
"from": "CUST-WP-0071-T03",
"target": "CUST-WP-0071-T01",
"relationship_type": "starts_after",
"from_workplan_id": "2249bddb-7524-5add-bd5c-c4163a6ca0f3",
"from_task_id": "6dc67558-eb1e-5bb6-a667-986f884dd495",
"to_workplan_id": null,
"to_task_id": "4ae8eb8d-f8c5-5256-ae50-c323cfd73d7a"
},
{
"workplan": "CUST-WP-0071",
"from": "CUST-WP-0071-T03",
"target": "CUST-WP-0071-T02",
"relationship_type": "starts_after",
"from_workplan_id": "2249bddb-7524-5add-bd5c-c4163a6ca0f3",
"from_task_id": "6dc67558-eb1e-5bb6-a667-986f884dd495",
"to_workplan_id": null,
"to_task_id": "82192370-2fd7-5363-88d2-3c67889d3d68"
},
{
"workplan": "CUST-WP-0071",
"from": "CUST-WP-0071-T02",
"target": "CUST-WP-0071-T01",
"relationship_type": "starts_after",
"from_workplan_id": "2249bddb-7524-5add-bd5c-c4163a6ca0f3",
"from_task_id": "82192370-2fd7-5363-88d2-3c67889d3d68",
"to_workplan_id": null,
"to_task_id": "4ae8eb8d-f8c5-5256-ae50-c323cfd73d7a"
},
{
"workplan": "CUST-WP-0071",
"from": "CUST-WP-0071-T02",
"target": "RAPPS-WP-0014-T03",
"relationship_type": "starts_after",
"from_workplan_id": "2249bddb-7524-5add-bd5c-c4163a6ca0f3",
"from_task_id": "82192370-2fd7-5363-88d2-3c67889d3d68",
"to_workplan_id": null,
"to_task_id": "dd069c6d-fcc1-5bac-b233-976f2f0d5cd1"
},
{
"workplan": "CUST-WP-0071",
"from": "CUST-WP-0071-T04",
"target": "CUST-WP-0071-T03",
"relationship_type": "starts_after",
"from_workplan_id": "2249bddb-7524-5add-bd5c-c4163a6ca0f3",
"from_task_id": "944f98e2-7e42-5894-925b-752423f3a1cc",
"to_workplan_id": null,
"to_task_id": "6dc67558-eb1e-5bb6-a667-986f884dd495"
},
{
"workplan": "CUST-WP-0073",
"from": "CUST-WP-0073-T02",
"target": "GLAS-WP-0012",
"relationship_type": "blocks",
"from_workplan_id": "a98a9f34-83b4-5c8c-8107-e05f7806d50d",
"from_task_id": "4b88b0b7-dc7e-5612-aa5d-1a08f0530c35",
"to_workplan_id": "170bf1ae-337f-5553-8d1e-03b07100e08f",
"to_task_id": null
},
{
"workplan": "CUST-WP-0073",
"from": "CUST-WP-0073-T04",
"target": "CUST-WP-0073-T02",
"relationship_type": "starts_after",
"from_workplan_id": "a98a9f34-83b4-5c8c-8107-e05f7806d50d",
"from_task_id": "90725511-4e31-549f-b567-47feff1a9ca4",
"to_workplan_id": null,
"to_task_id": "4b88b0b7-dc7e-5612-aa5d-1a08f0530c35"
},
{
"workplan": "CUST-WP-0079",
"from": "CUST-WP-0079-T03",
"target": "ACTIVITY-WP-0047",
"relationship_type": "blocks",
"from_workplan_id": "ab288f92-d364-5c24-9362-aee451f266c3",
"from_task_id": "1be0569c-221d-5925-a0ff-fd2af88e52b1",
"to_workplan_id": "ee97869b-dca2-54c9-aa18-0b479c244fb3",
"to_task_id": null
},
{
"workplan": "CUST-WP-0079",
"from": "CUST-WP-0079-T04",
"target": "PNEX-WP-0006",
"relationship_type": "blocks",
"from_workplan_id": "ab288f92-d364-5c24-9362-aee451f266c3",
"from_task_id": "5dd52156-09a8-5781-b288-99ae822f59a6",
"to_workplan_id": "774c5037-a3d0-5e65-893c-d18970acd66a",
"to_task_id": null
},
{
"workplan": "CUST-WP-0080",
"from": "CUST-WP-0080-T04",
"target": "CUST-WP-0080-T03",
"relationship_type": "starts_after",
"from_workplan_id": "dfadb19b-445e-59c6-bcd6-48e87b912c52",
"from_task_id": "82378b0f-7004-5c2f-9683-b356a1227576",
"to_workplan_id": null,
"to_task_id": "87637402-02f1-57c1-8fbc-4141598af8e3"
},
{
"workplan": "CUST-WP-0080",
"from": "CUST-WP-0080-T05",
"target": "RPF-WP-0056",
"relationship_type": "blocks",
"from_workplan_id": "dfadb19b-445e-59c6-bcd6-48e87b912c52",
"from_task_id": "ed8b909a-23bf-5e5f-b600-e477fbae136b",
"to_workplan_id": "31933d08-c877-5fda-aff9-6220c3705c92",
"to_task_id": null
},
{
"workplan": "CUST-WP-0080",
"from": "CUST-WP-0080-T05",
"target": "RAPP-PLAYWRIGHT-WP-0004",
"relationship_type": "blocks",
"from_workplan_id": "dfadb19b-445e-59c6-bcd6-48e87b912c52",
"from_task_id": "ed8b909a-23bf-5e5f-b600-e477fbae136b",
"to_workplan_id": "f9280c40-201e-59b5-a6f0-1aace8e0e2e2",
"to_task_id": null
}
],
"refused": [],
"notes": [
"Preserved bound historical row and task UUIDs: CUST-WP-ADHOC-2026-08-25-RETIRED"
]
}
}
}

View file

@ -0,0 +1,43 @@
{
"status": "committed",
"commit": "ceaeef1ee79eeba37e7cacf73ba8aed3357a5e7c",
"exit_code": 0,
"paths": [
".kaizen/agents/custodian-codex/supervision.json",
"docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/approval-decision.json",
"docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/check_live.py",
"docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/coordination-receipt.json",
"docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/execute.py",
"docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/execution.json",
"docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/fresh-headroom.json",
"docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/fresh-server-dryrun.json",
"docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/frozen-archives.json",
"docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/full-rows-after.json",
"docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/full-rows-before.json",
"docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/full-rows-post-pipeline.json",
"docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/helm-return.json",
"docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/job-observation.json",
"docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/native-binding-probes.json",
"docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/owner-bindings.json",
"docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/owner-heads.json",
"docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/publish_checkpoint.py",
"docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/run_maintenance.py",
"docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/runtime-after.json",
"docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/runtime-before.json",
"docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/staging-cleanup.json",
"docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/sync_custodian.py",
"docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/verified-client-cluster-consistency.json",
"docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/verified-client-cluster-projection.json",
"docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/verified-loaded-client.json",
"docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/verified-receiving-return.json",
"docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/verify_close.py",
"docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/verify_owner_bindings.py",
"docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/watch_rollout.py",
"docs/netkingdom-target-implementation.md",
"workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md",
"docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/publication-inputs.json"
],
"stdout": "[main ceaeef1] Record approved API guard delivery and completed owner returns\n 33 files changed, 2800 insertions(+), 9 deletions(-)\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/approval-decision.json\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/check_live.py\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/coordination-receipt.json\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/execute.py\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/execution.json\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/fresh-headroom.json\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/fresh-server-dryrun.json\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/frozen-archives.json\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/full-rows-after.json\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/full-rows-before.json\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/full-rows-post-pipeline.json\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/helm-return.json\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/job-observation.json\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/native-binding-probes.json\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/owner-bindings.json\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/owner-heads.json\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/publication-inputs.json\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/publish_checkpoint.py\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/run_maintenance.py\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/runtime-after.json\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/runtime-before.json\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/staging-cleanup.json\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/sync_custodian.py\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/verified-client-cluster-consistency.json\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/verified-client-cluster-projection.json\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/verified-loaded-client.json\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/verified-receiving-return.json\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/verify_close.py\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/verify_owner_bindings.py\n create mode 100644 docs/changes/netkingdom-target-2026-10-17/guard-successor-execution-r2/watch_rollout.py\n",
"stderr": "{\"blocked_blob_ids\": [], \"scanned_blobs\": 32, \"scanner_sha256\": \"88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\", \"scanner_version\": \"8.30.1\", \"scope\": \"changed-staged-blobs\", \"status\": \"clean\"}\n",
"unrelated_work_preserved": true
}

View file

@ -0,0 +1,9 @@
{
"status": "verified",
"directory": "/tmp/statehub-binding-guard-b5c0663d830f",
"uid": 1000,
"directory_mode": "700",
"file_mode": "600",
"archive_sha256": "b5c0663d830fc8d3b65080f237a0be8d01f8b2f7fec9dc33aeba91b1b6b281d4",
"remote_archive": "/tmp/statehub-binding-guard-b5c0663d830f/state-hub-0.1.0.tgz"
}

View file

@ -0,0 +1 @@
WARN: git commit failed for writeback: Generated-only commits rejected: staged