docs: preserve reviewed NetKingdom session artifacts
This commit is contained in:
parent
4a15141c69
commit
4c8f8d8c26
49 changed files with 8701 additions and 0 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 = ¤tClaimTransport{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
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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")
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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{})
|
||||
}
|
||||
|
|
@ -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 = ¤tClaimTransport{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
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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{})
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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")
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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{})
|
||||
}
|
||||
|
|
@ -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{})
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
|
|
@ -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))
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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))
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
|
@ -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:])
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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:])
|
||||
|
|
@ -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)}
|
||||
})}
|
||||
}
|
||||
|
|
@ -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")}
|
||||
}
|
||||
|
|
@ -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
|
||||
]
|
||||
}
|
||||
|
|
@ -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."
|
||||
]
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
WARN: working-memory commit failed: Generated-only commits rejected: staged
|
||||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
WARN: git commit failed for writeback: Generated-only commits rejected: staged
|
||||
Loading…
Add table
Add a link
Reference in a new issue