Compare commits

...

5 Commits

Author SHA1 Message Date
Brecht Van Lommel
87261f3fb9
Fix blame view missing lines (#22826)
Creating a new buffered reader for every part of the blame can miss
lines, as it will read and buffer bytes that the next buffered reader
will not get.

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-02-09 11:51:02 +08:00
John Olheiser
4dd7d61ac8
Load issue before accessing index in merge message (#22822)
Fixes #22821

Signed-off-by: jolheiser <john.olheiser@gmail.com>
2023-02-09 10:47:52 +08:00
Michal
5ae07d4c2f
include build info in Prometheus metrics (#22819)
Related to: https://github.com/go-gitea/gitea/issues/18061

This PR adds build info to the Prometheus metrics. This includes:
- goarch: https://pkg.go.dev/runtime#GOARCH
- goos: https://pkg.go.dev/runtime#pkg-constants
- goversion: https://pkg.go.dev/runtime#Version
- gitea version: just exposes the existing
code.gitea.io/gitea/modules/setting.AppVer

It's a similar approach to what some other Golang projects are doing,
e.g. Prometheus:
https://github.com/prometheus/common/blob/main/version/info.go

example /metrics response from Prometheus:
```
# HELP prometheus_build_info A metric with a constant '1' value labeled by version, revision, branch, goversion from which prometheus was built, and the goos and goarch for the build.
# TYPE prometheus_build_info gauge
prometheus_build_info{branch="HEAD",goarch="amd64",goos="linux",goversion="go1.19.4",revision="c0d8a56c69014279464c0e15d8bfb0e153af0dab",version="2.41.0"} 1
```

/metrics response from gitea with this PR:
```
# HELP gitea_build_info Build information
# TYPE gitea_build_info gauge
gitea_build_info{goarch="amd64",goos="linux",goversion="go1.20",version="2c6cc0b8c"} 1
```

Signed-off-by: Michal Wasilewski <mwasilewski@gmx.com>

<!--

Please check the following:

1. Make sure you are targeting the `main` branch, pull requests on
release branches are only allowed for bug fixes.
2. Read contributing guidelines:
https://github.com/go-gitea/gitea/blob/main/CONTRIBUTING.md
3. Describe what your pull request does and which issue you're targeting
(if any)

-->

Signed-off-by: Michal Wasilewski <mwasilewski@gmx.com>
2023-02-08 19:54:01 +02:00
Jason Song
7d3c4c3e8a
Fix rerun button of Actions (#22798)
When clicking the return button, the page should be refreshed. However,
the browser may cancel the previous fetch request, and it fails to rerun
the job. It's easier to reproduce the bug in Safari or Firefox than
Chrome for some reason.

<img width="384" alt="image"
src="https://user-images.githubusercontent.com/9418365/217142792-a783f9a1-7089-44db-b7d8-46c46c72d284.png">


<img width="752" alt="image"
src="https://user-images.githubusercontent.com/9418365/217132406-b8381b63-b323-474e-935b-2596b1b5c046.png">
2023-02-08 15:55:57 +08:00
KN4CK3R
e8186f1c0f
Map OIDC groups to Orgs/Teams (#21441)
Fixes #19555

Test-Instructions:
https://github.com/go-gitea/gitea/pull/21441#issuecomment-1419438000

This PR implements the mapping of user groups provided by OIDC providers
to orgs teams in Gitea. The main part is a refactoring of the existing
LDAP code to make it usable from different providers.

Refactorings:
- Moved the router auth code from module to service because of import
cycles
- Changed some model methods to take a `Context` parameter
- Moved the mapping code from LDAP to a common location

I've tested it with Keycloak but other providers should work too. The
JSON mapping format is the same as for LDAP.


![grafik](https://user-images.githubusercontent.com/1666336/195634392-3fc540fc-b229-4649-99ac-91ae8e19df2d.png)

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-02-08 14:44:42 +08:00
39 changed files with 558 additions and 448 deletions

View File

@ -372,6 +372,15 @@ var (
Value: "", Value: "",
Usage: "Group Claim value for restricted users", 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",
},
} }
microcmdAuthUpdateOauth = cli.Command{ microcmdAuthUpdateOauth = cli.Command{
@ -853,6 +862,8 @@ func parseOAuth2Config(c *cli.Context) *oauth2.Source {
GroupClaimName: c.String("group-claim-name"), GroupClaimName: c.String("group-claim-name"),
AdminGroup: c.String("admin-group"), AdminGroup: c.String("admin-group"),
RestrictedGroup: c.String("restricted-group"), RestrictedGroup: c.String("restricted-group"),
GroupTeamMap: c.String("group-team-map"),
GroupTeamMapRemoval: c.Bool("group-team-map-removal"),
} }
} }
@ -935,6 +946,12 @@ func runUpdateOauth(c *cli.Context) error {
if c.IsSet("restricted-group") { if c.IsSet("restricted-group") {
oAuth2Config.RestrictedGroup = c.String("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 // update custom URL mapping
customURLMapping := &oauth2.CustomURLMapping{} customURLMapping := &oauth2.CustomURLMapping{}

View File

@ -137,6 +137,8 @@ Admin operations:
- `--group-claim-name`: Claim name providing group names for this source. (Optional) - `--group-claim-name`: Claim name providing group names for this source. (Optional)
- `--admin-group`: Group Claim value for administrator users. (Optional) - `--admin-group`: Group Claim value for administrator users. (Optional)
- `--restricted-group`: Group Claim value for restricted users. (Optional) - `--restricted-group`: Group Claim value for restricted users. (Optional)
- `--group-team-map`: JSON mapping between groups and org teams. (Optional)
- `--group-team-map-removal`: Activate automatic team membership removal depending on groups. (Optional)
- Examples: - Examples:
- `gitea admin auth add-oauth --name external-github --provider github --key OBTAIN_FROM_SOURCE --secret OBTAIN_FROM_SOURCE` - `gitea admin auth add-oauth --name external-github --provider github --key OBTAIN_FROM_SOURCE --secret OBTAIN_FROM_SOURCE`
- `update-oauth`: - `update-oauth`:

View File

@ -110,22 +110,14 @@ func (org *Organization) CanCreateOrgRepo(uid int64) (bool, error) {
return CanCreateOrgRepo(db.DefaultContext, org.ID, uid) return CanCreateOrgRepo(db.DefaultContext, org.ID, uid)
} }
func (org *Organization) getTeam(ctx context.Context, name string) (*Team, error) { // GetTeam returns named team of organization.
func (org *Organization) GetTeam(ctx context.Context, name string) (*Team, error) {
return GetTeam(ctx, org.ID, name) return GetTeam(ctx, org.ID, name)
} }
// GetTeam returns named team of organization.
func (org *Organization) GetTeam(name string) (*Team, error) {
return org.getTeam(db.DefaultContext, name)
}
func (org *Organization) getOwnerTeam(ctx context.Context) (*Team, error) {
return org.getTeam(ctx, OwnerTeamName)
}
// GetOwnerTeam returns owner team of organization. // GetOwnerTeam returns owner team of organization.
func (org *Organization) GetOwnerTeam() (*Team, error) { func (org *Organization) GetOwnerTeam(ctx context.Context) (*Team, error) {
return org.getOwnerTeam(db.DefaultContext) return org.GetTeam(ctx, OwnerTeamName)
} }
// FindOrgTeams returns all teams of a given organization // FindOrgTeams returns all teams of a given organization
@ -342,7 +334,7 @@ func CreateOrganization(org *Organization, owner *user_model.User) (err error) {
} }
// GetOrgByName returns organization by given name. // GetOrgByName returns organization by given name.
func GetOrgByName(name string) (*Organization, error) { func GetOrgByName(ctx context.Context, name string) (*Organization, error) {
if len(name) == 0 { if len(name) == 0 {
return nil, ErrOrgNotExist{0, name} return nil, ErrOrgNotExist{0, name}
} }
@ -350,7 +342,7 @@ func GetOrgByName(name string) (*Organization, error) {
LowerName: strings.ToLower(name), LowerName: strings.ToLower(name),
Type: user_model.UserTypeOrganization, Type: user_model.UserTypeOrganization,
} }
has, err := db.GetEngine(db.DefaultContext).Get(u) has, err := db.GetEngine(ctx).Get(u)
if err != nil { if err != nil {
return nil, err return nil, err
} else if !has { } else if !has {

View File

@ -61,28 +61,28 @@ func TestUser_IsOrgMember(t *testing.T) {
func TestUser_GetTeam(t *testing.T) { func TestUser_GetTeam(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
team, err := org.GetTeam("team1") team, err := org.GetTeam(db.DefaultContext, "team1")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, org.ID, team.OrgID) assert.Equal(t, org.ID, team.OrgID)
assert.Equal(t, "team1", team.LowerName) assert.Equal(t, "team1", team.LowerName)
_, err = org.GetTeam("does not exist") _, err = org.GetTeam(db.DefaultContext, "does not exist")
assert.True(t, organization.IsErrTeamNotExist(err)) assert.True(t, organization.IsErrTeamNotExist(err))
nonOrg := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 2}) nonOrg := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 2})
_, err = nonOrg.GetTeam("team") _, err = nonOrg.GetTeam(db.DefaultContext, "team")
assert.True(t, organization.IsErrTeamNotExist(err)) assert.True(t, organization.IsErrTeamNotExist(err))
} }
func TestUser_GetOwnerTeam(t *testing.T) { func TestUser_GetOwnerTeam(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
team, err := org.GetOwnerTeam() team, err := org.GetOwnerTeam(db.DefaultContext)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, org.ID, team.OrgID) assert.Equal(t, org.ID, team.OrgID)
nonOrg := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 2}) nonOrg := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 2})
_, err = nonOrg.GetOwnerTeam() _, err = nonOrg.GetOwnerTeam(db.DefaultContext)
assert.True(t, organization.IsErrTeamNotExist(err)) assert.True(t, organization.IsErrTeamNotExist(err))
} }
@ -115,15 +115,15 @@ func TestUser_GetMembers(t *testing.T) {
func TestGetOrgByName(t *testing.T) { func TestGetOrgByName(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
org, err := organization.GetOrgByName("user3") org, err := organization.GetOrgByName(db.DefaultContext, "user3")
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, 3, org.ID) assert.EqualValues(t, 3, org.ID)
assert.Equal(t, "user3", org.Name) assert.Equal(t, "user3", org.Name)
_, err = organization.GetOrgByName("user2") // user2 is an individual _, err = organization.GetOrgByName(db.DefaultContext, "user2") // user2 is an individual
assert.True(t, organization.IsErrOrgNotExist(err)) assert.True(t, organization.IsErrOrgNotExist(err))
_, err = organization.GetOrgByName("") // corner case _, err = organization.GetOrgByName(db.DefaultContext, "") // corner case
assert.True(t, organization.IsErrOrgNotExist(err)) assert.True(t, organization.IsErrOrgNotExist(err))
} }

22
modules/auth/common.go Normal file
View File

@ -0,0 +1,22 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package auth
import (
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
)
func UnmarshalGroupTeamMapping(raw string) (map[string]map[string][]string, error) {
groupTeamMapping := make(map[string]map[string][]string)
if raw == "" {
return groupTeamMapping, nil
}
err := json.Unmarshal([]byte(raw), &groupTeamMapping)
if err != nil {
log.Error("Failed to unmarshal group team mapping: %v", err)
return nil, err
}
return groupTeamMapping, nil
}

View File

@ -19,7 +19,6 @@ import (
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/middleware"
auth_service "code.gitea.io/gitea/services/auth"
) )
// APIContext is a specific context for API service // APIContext is a specific context for API service
@ -215,35 +214,6 @@ func (ctx *APIContext) CheckForOTP() {
} }
} }
// APIAuth converts auth_service.Auth as a middleware
func APIAuth(authMethod auth_service.Method) func(*APIContext) {
return func(ctx *APIContext) {
// Get user from session if logged in.
var err error
ctx.Doer, err = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
if err != nil {
ctx.Error(http.StatusUnauthorized, "APIAuth", err)
return
}
if ctx.Doer != nil {
if ctx.Locale.Language() != ctx.Doer.Language {
ctx.Locale = middleware.Locale(ctx.Resp, ctx.Req)
}
ctx.IsBasicAuth = ctx.Data["AuthedMethod"].(string) == auth_service.BasicMethodName
ctx.IsSigned = true
ctx.Data["IsSigned"] = ctx.IsSigned
ctx.Data["SignedUser"] = ctx.Doer
ctx.Data["SignedUserID"] = ctx.Doer.ID
ctx.Data["SignedUserName"] = ctx.Doer.Name
ctx.Data["IsAdmin"] = ctx.Doer.IsAdmin
} else {
ctx.Data["SignedUserID"] = int64(0)
ctx.Data["SignedUserName"] = ""
}
}
}
// APIContexter returns apicontext as middleware // APIContexter returns apicontext as middleware
func APIContexter() func(http.Handler) http.Handler { func APIContexter() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {

View File

@ -36,7 +36,6 @@ import (
"code.gitea.io/gitea/modules/typesniffer" "code.gitea.io/gitea/modules/typesniffer"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/services/auth"
"gitea.com/go-chi/cache" "gitea.com/go-chi/cache"
"gitea.com/go-chi/session" "gitea.com/go-chi/session"
@ -659,37 +658,6 @@ func getCsrfOpts() CsrfOptions {
} }
} }
// Auth converts auth.Auth as a middleware
func Auth(authMethod auth.Method) func(*Context) {
return func(ctx *Context) {
var err error
ctx.Doer, err = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
if err != nil {
log.Error("Failed to verify user %v: %v", ctx.Req.RemoteAddr, err)
ctx.Error(http.StatusUnauthorized, "Verify")
return
}
if ctx.Doer != nil {
if ctx.Locale.Language() != ctx.Doer.Language {
ctx.Locale = middleware.Locale(ctx.Resp, ctx.Req)
}
ctx.IsBasicAuth = ctx.Data["AuthedMethod"].(string) == auth.BasicMethodName
ctx.IsSigned = true
ctx.Data["IsSigned"] = ctx.IsSigned
ctx.Data["SignedUser"] = ctx.Doer
ctx.Data["SignedUserID"] = ctx.Doer.ID
ctx.Data["SignedUserName"] = ctx.Doer.Name
ctx.Data["IsAdmin"] = ctx.Doer.IsAdmin
} else {
ctx.Data["SignedUserID"] = int64(0)
ctx.Data["SignedUserName"] = ""
// ensure the session uid is deleted
_ = ctx.Session.Delete("uid")
}
}
}
// Contexter initializes a classic context for a request. // Contexter initializes a classic context for a request.
func Contexter(ctx context.Context) func(next http.Handler) http.Handler { func Contexter(ctx context.Context) func(next http.Handler) http.Handler {
_, rnd := templates.HTMLRenderer(ctx) _, rnd := templates.HTMLRenderer(ctx)

View File

@ -80,7 +80,7 @@ func HandleOrgAssignment(ctx *Context, args ...bool) {
orgName := ctx.Params(":org") orgName := ctx.Params(":org")
var err error var err error
ctx.Org.Organization, err = organization.GetOrgByName(orgName) ctx.Org.Organization, err = organization.GetOrgByName(ctx, orgName)
if err != nil { if err != nil {
if organization.IsErrOrgNotExist(err) { if organization.IsErrOrgNotExist(err) {
redirectUserID, err := user_model.LookupUserRedirect(orgName) redirectUserID, err := user_model.LookupUserRedirect(orgName)

View File

@ -20,11 +20,12 @@ type BlamePart struct {
// BlameReader returns part of file blame one by one // BlameReader returns part of file blame one by one
type BlameReader struct { type BlameReader struct {
cmd *Command cmd *Command
output io.WriteCloser output io.WriteCloser
reader io.ReadCloser reader io.ReadCloser
done chan error bufferedReader *bufio.Reader
lastSha *string done chan error
lastSha *string
} }
var shaLineRegex = regexp.MustCompile("^([a-z0-9]{40})") var shaLineRegex = regexp.MustCompile("^([a-z0-9]{40})")
@ -33,8 +34,6 @@ var shaLineRegex = regexp.MustCompile("^([a-z0-9]{40})")
func (r *BlameReader) NextPart() (*BlamePart, error) { func (r *BlameReader) NextPart() (*BlamePart, error) {
var blamePart *BlamePart var blamePart *BlamePart
reader := bufio.NewReader(r.reader)
if r.lastSha != nil { if r.lastSha != nil {
blamePart = &BlamePart{*r.lastSha, make([]string, 0)} blamePart = &BlamePart{*r.lastSha, make([]string, 0)}
} }
@ -44,7 +43,7 @@ func (r *BlameReader) NextPart() (*BlamePart, error) {
var err error var err error
for err != io.EOF { for err != io.EOF {
line, isPrefix, err = reader.ReadLine() line, isPrefix, err = r.bufferedReader.ReadLine()
if err != nil && err != io.EOF { if err != nil && err != io.EOF {
return blamePart, err return blamePart, err
} }
@ -66,7 +65,7 @@ func (r *BlameReader) NextPart() (*BlamePart, error) {
r.lastSha = &sha1 r.lastSha = &sha1
// need to munch to end of line... // need to munch to end of line...
for isPrefix { for isPrefix {
_, isPrefix, err = reader.ReadLine() _, isPrefix, err = r.bufferedReader.ReadLine()
if err != nil && err != io.EOF { if err != nil && err != io.EOF {
return blamePart, err return blamePart, err
} }
@ -81,7 +80,7 @@ func (r *BlameReader) NextPart() (*BlamePart, error) {
// need to munch to end of line... // need to munch to end of line...
for isPrefix { for isPrefix {
_, isPrefix, err = reader.ReadLine() _, isPrefix, err = r.bufferedReader.ReadLine()
if err != nil && err != io.EOF { if err != nil && err != io.EOF {
return blamePart, err return blamePart, err
} }
@ -96,6 +95,7 @@ func (r *BlameReader) NextPart() (*BlamePart, error) {
// Close BlameReader - don't run NextPart after invoking that // Close BlameReader - don't run NextPart after invoking that
func (r *BlameReader) Close() error { func (r *BlameReader) Close() error {
err := <-r.done err := <-r.done
r.bufferedReader = nil
_ = r.reader.Close() _ = r.reader.Close()
_ = r.output.Close() _ = r.output.Close()
return err return err
@ -126,10 +126,13 @@ func CreateBlameReader(ctx context.Context, repoPath, commitID, file string) (*B
done <- err done <- err
}(cmd, repoPath, stdout, done) }(cmd, repoPath, stdout, done)
bufferedReader := bufio.NewReader(reader)
return &BlameReader{ return &BlameReader{
cmd: cmd, cmd: cmd,
output: stdout, output: stdout,
reader: reader, reader: reader,
done: done, bufferedReader: bufferedReader,
done: done,
}, nil }, nil
} }

View File

@ -28,7 +28,7 @@ func TestReadingBlameOutput(t *testing.T) {
}, },
{ {
"f32b0a9dfd09a60f616f29158f772cedd89942d2", "f32b0a9dfd09a60f616f29158f772cedd89942d2",
[]string{}, []string{"", "Do not make any changes to this repo it is used for unit testing"},
}, },
} }

View File

@ -4,7 +4,10 @@
package metrics package metrics
import ( import (
"runtime"
activities_model "code.gitea.io/gitea/models/activities" activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/modules/setting"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
) )
@ -17,6 +20,7 @@ type Collector struct {
Accesses *prometheus.Desc Accesses *prometheus.Desc
Actions *prometheus.Desc Actions *prometheus.Desc
Attachments *prometheus.Desc Attachments *prometheus.Desc
BuildInfo *prometheus.Desc
Comments *prometheus.Desc Comments *prometheus.Desc
Follows *prometheus.Desc Follows *prometheus.Desc
HookTasks *prometheus.Desc HookTasks *prometheus.Desc
@ -62,6 +66,16 @@ func NewCollector() Collector {
"Number of Attachments", "Number of Attachments",
nil, nil, nil, nil,
), ),
BuildInfo: prometheus.NewDesc(
namespace+"build_info",
"Build information",
[]string{
"goarch",
"goos",
"goversion",
"version",
}, nil,
),
Comments: prometheus.NewDesc( Comments: prometheus.NewDesc(
namespace+"comments", namespace+"comments",
"Number of Comments", "Number of Comments",
@ -195,6 +209,7 @@ func (c Collector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.Accesses ch <- c.Accesses
ch <- c.Actions ch <- c.Actions
ch <- c.Attachments ch <- c.Attachments
ch <- c.BuildInfo
ch <- c.Comments ch <- c.Comments
ch <- c.Follows ch <- c.Follows
ch <- c.HookTasks ch <- c.HookTasks
@ -241,6 +256,15 @@ func (c Collector) Collect(ch chan<- prometheus.Metric) {
prometheus.GaugeValue, prometheus.GaugeValue,
float64(stats.Counter.Attachment), float64(stats.Counter.Attachment),
) )
ch <- prometheus.MustNewConstMetric(
c.BuildInfo,
prometheus.GaugeValue,
1,
runtime.GOARCH,
runtime.GOOS,
runtime.Version(),
setting.AppVer,
)
ch <- prometheus.MustNewConstMetric( ch <- prometheus.MustNewConstMetric(
c.Comments, c.Comments,
prometheus.GaugeValue, prometheus.GaugeValue,

View File

@ -49,7 +49,7 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) {
assert.NoError(t, organization.CreateOrganization(org, user), "CreateOrganization") assert.NoError(t, organization.CreateOrganization(org, user), "CreateOrganization")
// Check Owner team. // Check Owner team.
ownerTeam, err := org.GetOwnerTeam() ownerTeam, err := org.GetOwnerTeam(db.DefaultContext)
assert.NoError(t, err, "GetOwnerTeam") assert.NoError(t, err, "GetOwnerTeam")
assert.True(t, ownerTeam.IncludesAllRepositories, "Owner team includes all repositories") assert.True(t, ownerTeam.IncludesAllRepositories, "Owner team includes all repositories")
@ -63,7 +63,7 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) {
} }
} }
// Get fresh copy of Owner team after creating repos. // Get fresh copy of Owner team after creating repos.
ownerTeam, err = org.GetOwnerTeam() ownerTeam, err = org.GetOwnerTeam(db.DefaultContext)
assert.NoError(t, err, "GetOwnerTeam") assert.NoError(t, err, "GetOwnerTeam")
// Create teams and check repositories. // Create teams and check repositories.

View File

@ -57,7 +57,7 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
repoPath := repo_model.RepoPath(u.Name, opts.RepoName) repoPath := repo_model.RepoPath(u.Name, opts.RepoName)
if u.IsOrganization() { if u.IsOrganization() {
t, err := organization.OrgFromUser(u).GetOwnerTeam() t, err := organization.OrgFromUser(u).GetOwnerTeam(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -8,6 +8,7 @@ import (
"regexp" "regexp"
"strings" "strings"
"code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"gitea.com/go-chi/binding" "gitea.com/go-chi/binding"
@ -17,15 +18,14 @@ import (
const ( const (
// ErrGitRefName is git reference name error // ErrGitRefName is git reference name error
ErrGitRefName = "GitRefNameError" ErrGitRefName = "GitRefNameError"
// ErrGlobPattern is returned when glob pattern is invalid // ErrGlobPattern is returned when glob pattern is invalid
ErrGlobPattern = "GlobPattern" ErrGlobPattern = "GlobPattern"
// ErrRegexPattern is returned when a regex pattern is invalid // ErrRegexPattern is returned when a regex pattern is invalid
ErrRegexPattern = "RegexPattern" ErrRegexPattern = "RegexPattern"
// ErrUsername is username error // ErrUsername is username error
ErrUsername = "UsernameError" ErrUsername = "UsernameError"
// ErrInvalidGroupTeamMap is returned when a group team mapping is invalid
ErrInvalidGroupTeamMap = "InvalidGroupTeamMap"
) )
// AddBindingRules adds additional binding rules // AddBindingRules adds additional binding rules
@ -37,6 +37,7 @@ func AddBindingRules() {
addRegexPatternRule() addRegexPatternRule()
addGlobOrRegexPatternRule() addGlobOrRegexPatternRule()
addUsernamePatternRule() addUsernamePatternRule()
addValidGroupTeamMapRule()
} }
func addGitRefNameBindingRule() { func addGitRefNameBindingRule() {
@ -167,6 +168,23 @@ func addUsernamePatternRule() {
}) })
} }
func addValidGroupTeamMapRule() {
binding.AddRule(&binding.Rule{
IsMatch: func(rule string) bool {
return strings.HasPrefix(rule, "ValidGroupTeamMap")
},
IsValid: func(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) {
_, err := auth.UnmarshalGroupTeamMapping(fmt.Sprintf("%v", val))
if err != nil {
errs.Add([]string{name}, ErrInvalidGroupTeamMap, err.Error())
return false, errs
}
return true, errs
},
})
}
func portOnly(hostport string) string { func portOnly(hostport string) string {
colon := strings.IndexByte(hostport, ':') colon := strings.IndexByte(hostport, ':')
if colon == -1 { if colon == -1 {

View File

@ -136,6 +136,8 @@ func Validate(errs binding.Errors, data map[string]interface{}, f Form, l transl
data["ErrorMsg"] = trName + l.Tr("form.regex_pattern_error", errs[0].Message) data["ErrorMsg"] = trName + l.Tr("form.regex_pattern_error", errs[0].Message)
case validation.ErrUsername: case validation.ErrUsername:
data["ErrorMsg"] = trName + l.Tr("form.username_error") data["ErrorMsg"] = trName + l.Tr("form.username_error")
case validation.ErrInvalidGroupTeamMap:
data["ErrorMsg"] = trName + l.Tr("form.invalid_group_team_map_error", errs[0].Message)
default: default:
msg := errs[0].Classification msg := errs[0].Classification
if msg != "" && errs[0].Message != "" { if msg != "" && errs[0].Message != "" {

View File

@ -477,6 +477,7 @@ include_error = ` must contain substring '%s'.`
glob_pattern_error = ` glob pattern is invalid: %s.` glob_pattern_error = ` glob pattern is invalid: %s.`
regex_pattern_error = ` regex pattern is invalid: %s.` regex_pattern_error = ` regex pattern is invalid: %s.`
username_error = ` can only contain alphanumeric chars ('0-9','a-z','A-Z'), dash ('-'), underscore ('_') and dot ('.'). It cannot begin or end with non-alphanumeric chars, and consecutive non-alphanumeric chars are also forbidden.` username_error = ` can only contain alphanumeric chars ('0-9','a-z','A-Z'), dash ('-'), underscore ('_') and dot ('.'). It cannot begin or end with non-alphanumeric chars, and consecutive non-alphanumeric chars are also forbidden.`
invalid_group_team_map_error = ` mapping is invalid: %s`
unknown_error = Unknown error: unknown_error = Unknown error:
captcha_incorrect = The CAPTCHA code is incorrect. captcha_incorrect = The CAPTCHA code is incorrect.
password_not_match = The passwords do not match. password_not_match = The passwords do not match.
@ -2758,6 +2759,8 @@ auths.oauth2_required_claim_value_helper = Set this value to restrict login from
auths.oauth2_group_claim_name = Claim name providing group names for this source. (Optional) auths.oauth2_group_claim_name = Claim name providing group names for this source. (Optional)
auths.oauth2_admin_group = Group Claim value for administrator users. (Optional - requires claim name above) auths.oauth2_admin_group = Group Claim value for administrator users. (Optional - requires claim name above)
auths.oauth2_restricted_group = Group Claim value for restricted users. (Optional - requires claim name above) auths.oauth2_restricted_group = Group Claim value for restricted users. (Optional - requires claim name above)
auths.oauth2_map_group_to_team = Map claimed groups to Organization teams. (Optional - requires claim name above)
auths.oauth2_map_group_to_team_removal = Remove users from synchronized teams if user does not belong to corresponding group.
auths.enable_auto_register = Enable Auto Registration auths.enable_auto_register = Enable Auto Registration
auths.sspi_auto_create_users = Automatically create users auths.sspi_auto_create_users = Automatically create users
auths.sspi_auto_create_users_helper = Allow SSPI auth method to automatically create new accounts for users that login for the first time auths.sspi_auto_create_users_helper = Allow SSPI auth method to automatically create new accounts for users that login for the first time

View File

@ -507,7 +507,7 @@ func orgAssignment(args ...bool) func(ctx *context.APIContext) {
var err error var err error
if assignOrg { if assignOrg {
ctx.Org.Organization, err = organization.GetOrgByName(ctx.Params(":org")) ctx.Org.Organization, err = organization.GetOrgByName(ctx, ctx.Params(":org"))
if err != nil { if err != nil {
if organization.IsErrOrgNotExist(err) { if organization.IsErrOrgNotExist(err) {
redirectUserID, err := user_model.LookupUserRedirect(ctx.Params(":org")) redirectUserID, err := user_model.LookupUserRedirect(ctx.Params(":org"))
@ -687,7 +687,7 @@ func Routes(ctx gocontext.Context) *web.Route {
} }
// Get user from session if logged in. // Get user from session if logged in.
m.Use(context.APIAuth(group)) m.Use(auth.APIAuth(group))
m.Use(context.ToggleAPI(&context.ToggleOptions{ m.Use(context.ToggleAPI(&context.ToggleOptions{
SignInRequired: setting.Service.RequireSignInView, SignInRequired: setting.Service.RequireSignInView,

View File

@ -108,7 +108,7 @@ func CreateFork(ctx *context.APIContext) {
if form.Organization == nil { if form.Organization == nil {
forker = ctx.Doer forker = ctx.Doer
} else { } else {
org, err := organization.GetOrgByName(*form.Organization) org, err := organization.GetOrgByName(ctx, *form.Organization)
if err != nil { if err != nil {
if organization.IsErrOrgNotExist(err) { if organization.IsErrOrgNotExist(err) {
ctx.Error(http.StatusUnprocessableEntity, "", err) ctx.Error(http.StatusUnprocessableEntity, "", err)

View File

@ -468,7 +468,7 @@ func CreateOrgRepo(ctx *context.APIContext) {
// "403": // "403":
// "$ref": "#/responses/forbidden" // "$ref": "#/responses/forbidden"
opt := web.GetForm(ctx).(*api.CreateRepoOption) opt := web.GetForm(ctx).(*api.CreateRepoOption)
org, err := organization.GetOrgByName(ctx.Params(":org")) org, err := organization.GetOrgByName(ctx, ctx.Params(":org"))
if err != nil { if err != nil {
if organization.IsErrOrgNotExist(err) { if organization.IsErrOrgNotExist(err) {
ctx.Error(http.StatusUnprocessableEntity, "", err) ctx.Error(http.StatusUnprocessableEntity, "", err)

View File

@ -204,6 +204,8 @@ func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source {
GroupClaimName: form.Oauth2GroupClaimName, GroupClaimName: form.Oauth2GroupClaimName,
RestrictedGroup: form.Oauth2RestrictedGroup, RestrictedGroup: form.Oauth2RestrictedGroup,
AdminGroup: form.Oauth2AdminGroup, AdminGroup: form.Oauth2AdminGroup,
GroupTeamMap: form.Oauth2GroupTeamMap,
GroupTeamMapRemoval: form.Oauth2GroupTeamMapRemoval,
} }
} }

View File

@ -16,6 +16,7 @@ import (
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
auth_service "code.gitea.io/gitea/services/auth" auth_service "code.gitea.io/gitea/services/auth"
"code.gitea.io/gitea/services/auth/source/oauth2"
"code.gitea.io/gitea/services/externalaccount" "code.gitea.io/gitea/services/externalaccount"
"code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/forms"
@ -267,5 +268,11 @@ func LinkAccountPostRegister(ctx *context.Context) {
return return
} }
source := authSource.Cfg.(*oauth2.Source)
if err := syncGroupsToTeams(ctx, source, &gothUser, u); err != nil {
ctx.ServerError("SyncGroupsToTeams", err)
return
}
handleSignIn(ctx, u, false) handleSignIn(ctx, u, false)
} }

View File

@ -17,7 +17,9 @@ import (
"code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/auth"
org_model "code.gitea.io/gitea/models/organization" org_model "code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
auth_module "code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
@ -27,6 +29,7 @@ import (
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/middleware"
auth_service "code.gitea.io/gitea/services/auth" auth_service "code.gitea.io/gitea/services/auth"
source_service "code.gitea.io/gitea/services/auth/source"
"code.gitea.io/gitea/services/auth/source/oauth2" "code.gitea.io/gitea/services/auth/source/oauth2"
"code.gitea.io/gitea/services/externalaccount" "code.gitea.io/gitea/services/externalaccount"
"code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/forms"
@ -963,12 +966,19 @@ func SignInOAuthCallback(ctx *context.Context) {
IsActive: util.OptionalBoolOf(!setting.OAuth2Client.RegisterEmailConfirm), IsActive: util.OptionalBoolOf(!setting.OAuth2Client.RegisterEmailConfirm),
} }
setUserGroupClaims(authSource, u, &gothUser) source := authSource.Cfg.(*oauth2.Source)
setUserAdminAndRestrictedFromGroupClaims(source, u, &gothUser)
if !createAndHandleCreatedUser(ctx, base.TplName(""), nil, u, overwriteDefault, &gothUser, setting.OAuth2Client.AccountLinking != setting.OAuth2AccountLinkingDisabled) { if !createAndHandleCreatedUser(ctx, base.TplName(""), nil, u, overwriteDefault, &gothUser, setting.OAuth2Client.AccountLinking != setting.OAuth2AccountLinkingDisabled) {
// error already handled // error already handled
return return
} }
if err := syncGroupsToTeams(ctx, source, &gothUser, u); err != nil {
ctx.ServerError("SyncGroupsToTeams", err)
return
}
} else { } else {
// no existing user is found, request attach or new account // no existing user is found, request attach or new account
showLinkingLogin(ctx, gothUser) showLinkingLogin(ctx, gothUser)
@ -979,7 +989,7 @@ func SignInOAuthCallback(ctx *context.Context) {
handleOAuth2SignIn(ctx, authSource, u, gothUser) handleOAuth2SignIn(ctx, authSource, u, gothUser)
} }
func claimValueToStringSlice(claimValue interface{}) []string { func claimValueToStringSet(claimValue interface{}) container.Set[string] {
var groups []string var groups []string
switch rawGroup := claimValue.(type) { switch rawGroup := claimValue.(type) {
@ -993,37 +1003,45 @@ func claimValueToStringSlice(claimValue interface{}) []string {
str := fmt.Sprintf("%s", rawGroup) str := fmt.Sprintf("%s", rawGroup)
groups = strings.Split(str, ",") groups = strings.Split(str, ",")
} }
return groups return container.SetOf(groups...)
} }
func setUserGroupClaims(loginSource *auth.Source, u *user_model.User, gothUser *goth.User) bool { func syncGroupsToTeams(ctx *context.Context, source *oauth2.Source, gothUser *goth.User, u *user_model.User) error {
source := loginSource.Cfg.(*oauth2.Source) if source.GroupTeamMap != "" || source.GroupTeamMapRemoval {
if source.GroupClaimName == "" || (source.AdminGroup == "" && source.RestrictedGroup == "") { groupTeamMapping, err := auth_module.UnmarshalGroupTeamMapping(source.GroupTeamMap)
return false if err != nil {
return err
}
groups := getClaimedGroups(source, gothUser)
if err := source_service.SyncGroupsToTeams(ctx, u, groups, groupTeamMapping, source.GroupTeamMapRemoval); err != nil {
return err
}
} }
return nil
}
func getClaimedGroups(source *oauth2.Source, gothUser *goth.User) container.Set[string] {
groupClaims, has := gothUser.RawData[source.GroupClaimName] groupClaims, has := gothUser.RawData[source.GroupClaimName]
if !has { if !has {
return false return nil
} }
groups := claimValueToStringSlice(groupClaims) return claimValueToStringSet(groupClaims)
}
func setUserAdminAndRestrictedFromGroupClaims(source *oauth2.Source, u *user_model.User, gothUser *goth.User) bool {
groups := getClaimedGroups(source, gothUser)
wasAdmin, wasRestricted := u.IsAdmin, u.IsRestricted wasAdmin, wasRestricted := u.IsAdmin, u.IsRestricted
if source.AdminGroup != "" { if source.AdminGroup != "" {
u.IsAdmin = false u.IsAdmin = groups.Contains(source.AdminGroup)
} }
if source.RestrictedGroup != "" { if source.RestrictedGroup != "" {
u.IsRestricted = false u.IsRestricted = groups.Contains(source.RestrictedGroup)
}
for _, g := range groups {
if source.AdminGroup != "" && g == source.AdminGroup {
u.IsAdmin = true
} else if source.RestrictedGroup != "" && g == source.RestrictedGroup {
u.IsRestricted = true
}
} }
return wasAdmin != u.IsAdmin || wasRestricted != u.IsRestricted return wasAdmin != u.IsAdmin || wasRestricted != u.IsRestricted
@ -1070,6 +1088,15 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
needs2FA = err == nil needs2FA = err == nil
} }
oauth2Source := source.Cfg.(*oauth2.Source)
groupTeamMapping, err := auth_module.UnmarshalGroupTeamMapping(oauth2Source.GroupTeamMap)
if err != nil {
ctx.ServerError("UnmarshalGroupTeamMapping", err)
return
}
groups := getClaimedGroups(oauth2Source, &gothUser)
// If this user is enrolled in 2FA and this source doesn't override it, // If this user is enrolled in 2FA and this source doesn't override it,
// we can't sign the user in just yet. Instead, redirect them to the 2FA authentication page. // we can't sign the user in just yet. Instead, redirect them to the 2FA authentication page.
if !needs2FA { if !needs2FA {
@ -1088,7 +1115,7 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
u.SetLastLogin() u.SetLastLogin()
// Update GroupClaims // Update GroupClaims
changed := setUserGroupClaims(source, u, &gothUser) changed := setUserAdminAndRestrictedFromGroupClaims(oauth2Source, u, &gothUser)
cols := []string{"last_login_unix"} cols := []string{"last_login_unix"}
if changed { if changed {
cols = append(cols, "is_admin", "is_restricted") cols = append(cols, "is_admin", "is_restricted")
@ -1099,6 +1126,13 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
return return
} }
if oauth2Source.GroupTeamMap != "" || oauth2Source.GroupTeamMapRemoval {
if err := source_service.SyncGroupsToTeams(ctx, u, groups, groupTeamMapping, oauth2Source.GroupTeamMapRemoval); err != nil {
ctx.ServerError("SyncGroupsToTeams", err)
return
}
}
// update external user information // update external user information
if err := externalaccount.UpdateExternalUser(u, gothUser); err != nil { if err := externalaccount.UpdateExternalUser(u, gothUser); err != nil {
if !errors.Is(err, util.ErrNotExist) { if !errors.Is(err, util.ErrNotExist) {
@ -1121,7 +1155,7 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
return return
} }
changed := setUserGroupClaims(source, u, &gothUser) changed := setUserAdminAndRestrictedFromGroupClaims(oauth2Source, u, &gothUser)
if changed { if changed {
if err := user_model.UpdateUserCols(ctx, u, "is_admin", "is_restricted"); err != nil { if err := user_model.UpdateUserCols(ctx, u, "is_admin", "is_restricted"); err != nil {
ctx.ServerError("UpdateUserCols", err) ctx.ServerError("UpdateUserCols", err)
@ -1129,6 +1163,13 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
} }
} }
if oauth2Source.GroupTeamMap != "" || oauth2Source.GroupTeamMapRemoval {
if err := source_service.SyncGroupsToTeams(ctx, u, groups, groupTeamMapping, oauth2Source.GroupTeamMapRemoval); err != nil {
ctx.ServerError("SyncGroupsToTeams", err)
return
}
}
if err := updateSession(ctx, nil, map[string]interface{}{ if err := updateSession(ctx, nil, map[string]interface{}{
// User needs to use 2FA, save data and redirect to 2FA page. // User needs to use 2FA, save data and redirect to 2FA page.
"twofaUid": u.ID, "twofaUid": u.ID,
@ -1188,15 +1229,9 @@ func oAuth2UserLoginCallback(authSource *auth.Source, request *http.Request, res
} }
if oauth2Source.RequiredClaimValue != "" { if oauth2Source.RequiredClaimValue != "" {
groups := claimValueToStringSlice(claimInterface) groups := claimValueToStringSet(claimInterface)
found := false
for _, group := range groups { if !groups.Contains(oauth2Source.RequiredClaimValue) {
if group == oauth2Source.RequiredClaimValue {
found = true
break
}
}
if !found {
return nil, goth.User{}, user_model.ErrUserProhibitLogin{Name: gothUser.UserID} return nil, goth.User{}, user_model.ErrUserProhibitLogin{Name: gothUser.UserID}
} }
} }

View File

@ -78,7 +78,7 @@ func RetrieveLabels(ctx *context.Context) {
} }
ctx.Data["OrgLabels"] = orgLabels ctx.Data["OrgLabels"] = orgLabels
org, err := organization.GetOrgByName(ctx.Repo.Owner.LowerName) org, err := organization.GetOrgByName(ctx, ctx.Repo.Owner.LowerName)
if err != nil { if err != nil {
ctx.ServerError("GetOrgByName", err) ctx.ServerError("GetOrgByName", err)
return return

View File

@ -1006,7 +1006,7 @@ func AddTeamPost(ctx *context.Context) {
return return
} }
team, err := organization.OrgFromUser(ctx.Repo.Owner).GetTeam(name) team, err := organization.OrgFromUser(ctx.Repo.Owner).GetTeam(ctx, name)
if err != nil { if err != nil {
if organization.IsErrTeamNotExist(err) { if organization.IsErrTeamNotExist(err) {
ctx.Flash.Error(ctx.Tr("form.team_not_exist")) ctx.Flash.Error(ctx.Tr("form.team_not_exist"))

View File

@ -203,7 +203,7 @@ func Routes(ctx gocontext.Context) *web.Route {
} }
// Get user from session if logged in. // Get user from session if logged in.
common = append(common, context.Auth(group)) common = append(common, auth_service.Auth(group))
// GetHead allows a HEAD request redirect to GET if HEAD method is not defined for that route // GetHead allows a HEAD request redirect to GET if HEAD method is not defined for that route
common = append(common, middleware.GetHead) common = append(common, middleware.GetHead)

View File

@ -0,0 +1,60 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package auth
import (
"net/http"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/web/middleware"
)
// Auth is a middleware to authenticate a web user
func Auth(authMethod Method) func(*context.Context) {
return func(ctx *context.Context) {
if err := authShared(ctx, authMethod); err != nil {
log.Error("Failed to verify user: %v", err)
ctx.Error(http.StatusUnauthorized, "Verify")
return
}
if ctx.Doer == nil {
// ensure the session uid is deleted
_ = ctx.Session.Delete("uid")
}
}
}
// APIAuth is a middleware to authenticate an api user
func APIAuth(authMethod Method) func(*context.APIContext) {
return func(ctx *context.APIContext) {
if err := authShared(ctx.Context, authMethod); err != nil {
ctx.Error(http.StatusUnauthorized, "APIAuth", err)
}
}
}
func authShared(ctx *context.Context, authMethod Method) error {
var err error
ctx.Doer, err = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
if err != nil {
return err
}
if ctx.Doer != nil {
if ctx.Locale.Language() != ctx.Doer.Language {
ctx.Locale = middleware.Locale(ctx.Resp, ctx.Req)
}
ctx.IsBasicAuth = ctx.Data["AuthedMethod"].(string) == BasicMethodName
ctx.IsSigned = true
ctx.Data["IsSigned"] = ctx.IsSigned
ctx.Data["SignedUser"] = ctx.Doer
ctx.Data["SignedUserID"] = ctx.Doer.ID
ctx.Data["SignedUserName"] = ctx.Doer.Name
ctx.Data["IsAdmin"] = ctx.Doer.IsAdmin
} else {
ctx.Data["SignedUserID"] = int64(0)
ctx.Data["SignedUserName"] = ""
}
return nil
}

View File

@ -10,9 +10,10 @@ import (
asymkey_model "code.gitea.io/gitea/models/asymkey" asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
auth_module "code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
source_service "code.gitea.io/gitea/services/auth/source"
"code.gitea.io/gitea/services/mailer" "code.gitea.io/gitea/services/mailer"
user_service "code.gitea.io/gitea/services/user" user_service "code.gitea.io/gitea/services/user"
) )
@ -64,61 +65,66 @@ func (source *Source) Authenticate(user *user_model.User, userName, password str
} }
if user != nil { if user != nil {
if source.GroupsEnabled && (source.GroupTeamMap != "" || source.GroupTeamMapRemoval) {
orgCache := make(map[string]*organization.Organization)
teamCache := make(map[string]*organization.Team)
source.SyncLdapGroupsToTeams(user, sr.LdapTeamAdd, sr.LdapTeamRemove, orgCache, teamCache)
}
if isAttributeSSHPublicKeySet && asymkey_model.SynchronizePublicKeys(user, source.authSource, sr.SSHPublicKey) { if isAttributeSSHPublicKeySet && asymkey_model.SynchronizePublicKeys(user, source.authSource, sr.SSHPublicKey) {
return user, asymkey_model.RewriteAllPublicKeys() if err := asymkey_model.RewriteAllPublicKeys(); err != nil {
return user, err
}
}
} else {
// Fallback.
if len(sr.Username) == 0 {
sr.Username = userName
}
if len(sr.Mail) == 0 {
sr.Mail = fmt.Sprintf("%s@localhost", sr.Username)
}
user = &user_model.User{
LowerName: strings.ToLower(sr.Username),
Name: sr.Username,
FullName: composeFullName(sr.Name, sr.Surname, sr.Username),
Email: sr.Mail,
LoginType: source.authSource.Type,
LoginSource: source.authSource.ID,
LoginName: userName,
IsAdmin: sr.IsAdmin,
}
overwriteDefault := &user_model.CreateUserOverwriteOptions{
IsRestricted: util.OptionalBoolOf(sr.IsRestricted),
IsActive: util.OptionalBoolTrue,
}
err := user_model.CreateUser(user, overwriteDefault)
if err != nil {
return user, err
}
mailer.SendRegisterNotifyMail(user)
if isAttributeSSHPublicKeySet && asymkey_model.AddPublicKeysBySource(user, source.authSource, sr.SSHPublicKey) {
if err := asymkey_model.RewriteAllPublicKeys(); err != nil {
return user, err
}
}
if len(source.AttributeAvatar) > 0 {
if err := user_service.UploadAvatar(user, sr.Avatar); err != nil {
return user, err
}
} }
return user, nil
} }
// Fallback.
if len(sr.Username) == 0 {
sr.Username = userName
}
if len(sr.Mail) == 0 {
sr.Mail = fmt.Sprintf("%s@localhost", sr.Username)
}
user = &user_model.User{
LowerName: strings.ToLower(sr.Username),
Name: sr.Username,
FullName: composeFullName(sr.Name, sr.Surname, sr.Username),
Email: sr.Mail,
LoginType: source.authSource.Type,
LoginSource: source.authSource.ID,
LoginName: userName,
IsAdmin: sr.IsAdmin,
}
overwriteDefault := &user_model.CreateUserOverwriteOptions{
IsRestricted: util.OptionalBoolOf(sr.IsRestricted),
IsActive: util.OptionalBoolTrue,
}
err := user_model.CreateUser(user, overwriteDefault)
if err != nil {
return user, err
}
mailer.SendRegisterNotifyMail(user)
if isAttributeSSHPublicKeySet && asymkey_model.AddPublicKeysBySource(user, source.authSource, sr.SSHPublicKey) {
err = asymkey_model.RewriteAllPublicKeys()
}
if err == nil && len(source.AttributeAvatar) > 0 {
_ = user_service.UploadAvatar(user, sr.Avatar)
}
if source.GroupsEnabled && (source.GroupTeamMap != "" || source.GroupTeamMapRemoval) { if source.GroupsEnabled && (source.GroupTeamMap != "" || source.GroupTeamMapRemoval) {
orgCache := make(map[string]*organization.Organization) groupTeamMapping, err := auth_module.UnmarshalGroupTeamMapping(source.GroupTeamMap)
teamCache := make(map[string]*organization.Team) if err != nil {
source.SyncLdapGroupsToTeams(user, sr.LdapTeamAdd, sr.LdapTeamRemove, orgCache, teamCache) return user, err
}
if err := source_service.SyncGroupsToTeams(db.DefaultContext, user, sr.Groups, groupTeamMapping, source.GroupTeamMapRemoval); err != nil {
return user, err
}
} }
return user, err return user, nil
} }
// IsSkipLocalTwoFA returns if this source should skip local 2fa for password authentication // IsSkipLocalTwoFA returns if this source should skip local 2fa for password authentication

View File

@ -1,94 +0,0 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package ldap
import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
)
// SyncLdapGroupsToTeams maps LDAP groups to organization and team memberships
func (source *Source) SyncLdapGroupsToTeams(user *user_model.User, ldapTeamAdd, ldapTeamRemove map[string][]string, orgCache map[string]*organization.Organization, teamCache map[string]*organization.Team) {
var err error
if source.GroupsEnabled && source.GroupTeamMapRemoval {
// when the user is not a member of configs LDAP group, remove mapped organizations/teams memberships
removeMappedMemberships(user, ldapTeamRemove, orgCache, teamCache)
}
for orgName, teamNames := range ldapTeamAdd {
org, ok := orgCache[orgName]
if !ok {
org, err = organization.GetOrgByName(orgName)
if err != nil {
// organization must be created before LDAP group sync
log.Warn("LDAP group sync: Could not find organisation %s: %v", orgName, err)
continue
}
orgCache[orgName] = org
}
for _, teamName := range teamNames {
team, ok := teamCache[orgName+teamName]
if !ok {
team, err = org.GetTeam(teamName)
if err != nil {
// team must be created before LDAP group sync
log.Warn("LDAP group sync: Could not find team %s: %v", teamName, err)
continue
}
teamCache[orgName+teamName] = team
}
if isMember, err := organization.IsTeamMember(db.DefaultContext, org.ID, team.ID, user.ID); !isMember && err == nil {
log.Trace("LDAP group sync: adding user [%s] to team [%s]", user.Name, org.Name)
} else {
continue
}
err := models.AddTeamMember(team, user.ID)
if err != nil {
log.Error("LDAP group sync: Could not add user to team: %v", err)
}
}
}
}
// remove membership to organizations/teams if user is not member of corresponding LDAP group
// e.g. lets assume user is member of LDAP group "x", but LDAP group team map contains LDAP groups "x" and "y"
// then users membership gets removed for all organizations/teams mapped by LDAP group "y"
func removeMappedMemberships(user *user_model.User, ldapTeamRemove map[string][]string, orgCache map[string]*organization.Organization, teamCache map[string]*organization.Team) {
var err error
for orgName, teamNames := range ldapTeamRemove {
org, ok := orgCache[orgName]
if !ok {
org, err = organization.GetOrgByName(orgName)
if err != nil {
// organization must be created before LDAP group sync
log.Warn("LDAP group sync: Could not find organisation %s: %v", orgName, err)
continue
}
orgCache[orgName] = org
}
for _, teamName := range teamNames {
team, ok := teamCache[orgName+teamName]
if !ok {
team, err = org.GetTeam(teamName)
if err != nil {
// team must must be created before LDAP group sync
log.Warn("LDAP group sync: Could not find team %s: %v", teamName, err)
continue
}
}
if isMember, err := organization.IsTeamMember(db.DefaultContext, org.ID, team.ID, user.ID); isMember && err == nil {
log.Trace("LDAP group sync: removing user [%s] from team [%s]", user.Name, org.Name)
} else {
continue
}
err = models.RemoveTeamMember(team, user.ID)
if err != nil {
log.Error("LDAP group sync: Could not remove user from team: %v", err)
}
}
}
}

View File

@ -11,26 +11,24 @@ import (
"strconv" "strconv"
"strings" "strings"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
"github.com/go-ldap/ldap/v3" "github.com/go-ldap/ldap/v3"
) )
// SearchResult : user data // SearchResult : user data
type SearchResult struct { type SearchResult struct {
Username string // Username Username string // Username
Name string // Name Name string // Name
Surname string // Surname Surname string // Surname
Mail string // E-mail address Mail string // E-mail address
SSHPublicKey []string // SSH Public Key SSHPublicKey []string // SSH Public Key
IsAdmin bool // if user is administrator IsAdmin bool // if user is administrator
IsRestricted bool // if user is restricted IsRestricted bool // if user is restricted
LowerName string // LowerName LowerName string // LowerName
Avatar []byte Avatar []byte
LdapTeamAdd map[string][]string // organizations teams to add Groups container.Set[string]
LdapTeamRemove map[string][]string // organizations teams to remove
} }
func (source *Source) sanitizedUserQuery(username string) (string, bool) { func (source *Source) sanitizedUserQuery(username string) (string, bool) {
@ -196,9 +194,8 @@ func checkRestricted(l *ldap.Conn, ls *Source, userDN string) bool {
} }
// List all group memberships of a user // List all group memberships of a user
func (source *Source) listLdapGroupMemberships(l *ldap.Conn, uid string, applyGroupFilter bool) []string { func (source *Source) listLdapGroupMemberships(l *ldap.Conn, uid string, applyGroupFilter bool) container.Set[string] {
var ldapGroups []string ldapGroups := make(container.Set[string])
var searchFilter string
groupFilter, ok := source.sanitizedGroupFilter(source.GroupFilter) groupFilter, ok := source.sanitizedGroupFilter(source.GroupFilter)
if !ok { if !ok {
@ -210,12 +207,12 @@ func (source *Source) listLdapGroupMemberships(l *ldap.Conn, uid string, applyGr
return ldapGroups return ldapGroups
} }
var searchFilter string
if applyGroupFilter { if applyGroupFilter {
searchFilter = fmt.Sprintf("(&(%s)(%s=%s))", groupFilter, source.GroupMemberUID, ldap.EscapeFilter(uid)) searchFilter = fmt.Sprintf("(&(%s)(%s=%s))", groupFilter, source.GroupMemberUID, ldap.EscapeFilter(uid))
} else { } else {
searchFilter = fmt.Sprintf("(%s=%s)", source.GroupMemberUID, ldap.EscapeFilter(uid)) searchFilter = fmt.Sprintf("(%s=%s)", source.GroupMemberUID, ldap.EscapeFilter(uid))
} }
result, err := l.Search(ldap.NewSearchRequest( result, err := l.Search(ldap.NewSearchRequest(
groupDN, groupDN,
ldap.ScopeWholeSubtree, ldap.ScopeWholeSubtree,
@ -237,44 +234,12 @@ func (source *Source) listLdapGroupMemberships(l *ldap.Conn, uid string, applyGr
log.Error("LDAP search was successful, but found no DN!") log.Error("LDAP search was successful, but found no DN!")
continue continue
} }
ldapGroups = append(ldapGroups, entry.DN) ldapGroups.Add(entry.DN)
} }
return ldapGroups return ldapGroups
} }
// parse LDAP groups and return map of ldap groups to organizations teams
func (source *Source) mapLdapGroupsToTeams() map[string]map[string][]string {
ldapGroupsToTeams := make(map[string]map[string][]string)
err := json.Unmarshal([]byte(source.GroupTeamMap), &ldapGroupsToTeams)
if err != nil {
log.Error("Failed to unmarshall LDAP teams map: %v", err)
return ldapGroupsToTeams
}
return ldapGroupsToTeams
}
// getMappedMemberships : returns the organizations and teams to modify the users membership
func (source *Source) getMappedMemberships(usersLdapGroups []string, uid string) (map[string][]string, map[string][]string) {
// unmarshall LDAP group team map from configs
ldapGroupsToTeams := source.mapLdapGroupsToTeams()
membershipsToAdd := map[string][]string{}
membershipsToRemove := map[string][]string{}
for group, memberships := range ldapGroupsToTeams {
isUserInGroup := util.SliceContainsString(usersLdapGroups, group)
if isUserInGroup {
for org, teams := range memberships {
membershipsToAdd[org] = teams
}
} else if !isUserInGroup {
for org, teams := range memberships {
membershipsToRemove[org] = teams
}
}
}
return membershipsToAdd, membershipsToRemove
}
func (source *Source) getUserAttributeListedInGroup(entry *ldap.Entry) string { func (source *Source) getUserAttributeListedInGroup(entry *ldap.Entry) string {
if strings.ToLower(source.UserUID) == "dn" { if strings.ToLower(source.UserUID) == "dn" {
return entry.DN return entry.DN
@ -399,23 +364,6 @@ func (source *Source) SearchEntry(name, passwd string, directBind bool) *SearchR
surname := sr.Entries[0].GetAttributeValue(source.AttributeSurname) surname := sr.Entries[0].GetAttributeValue(source.AttributeSurname)
mail := sr.Entries[0].GetAttributeValue(source.AttributeMail) mail := sr.Entries[0].GetAttributeValue(source.AttributeMail)
teamsToAdd := make(map[string][]string)
teamsToRemove := make(map[string][]string)
// Check group membership
if source.GroupsEnabled {
userAttributeListedInGroup := source.getUserAttributeListedInGroup(sr.Entries[0])
usersLdapGroups := source.listLdapGroupMemberships(l, userAttributeListedInGroup, true)
if source.GroupFilter != "" && len(usersLdapGroups) == 0 {
return nil
}
if source.GroupTeamMap != "" || source.GroupTeamMapRemoval {
teamsToAdd, teamsToRemove = source.getMappedMemberships(usersLdapGroups, userAttributeListedInGroup)
}
}
if isAttributeSSHPublicKeySet { if isAttributeSSHPublicKeySet {
sshPublicKey = sr.Entries[0].GetAttributeValues(source.AttributeSSHPublicKey) sshPublicKey = sr.Entries[0].GetAttributeValues(source.AttributeSSHPublicKey)
} }
@ -431,6 +379,17 @@ func (source *Source) SearchEntry(name, passwd string, directBind bool) *SearchR
Avatar = sr.Entries[0].GetRawAttributeValue(source.AttributeAvatar) Avatar = sr.Entries[0].GetRawAttributeValue(source.AttributeAvatar)
} }
// Check group membership
var usersLdapGroups container.Set[string]
if source.GroupsEnabled {
userAttributeListedInGroup := source.getUserAttributeListedInGroup(sr.Entries[0])
usersLdapGroups = source.listLdapGroupMemberships(l, userAttributeListedInGroup, true)
if source.GroupFilter != "" && len(usersLdapGroups) == 0 {
return nil
}
}
if !directBind && source.AttributesInBind { if !directBind && source.AttributesInBind {
// binds user (checking password) after looking-up attributes in BindDN context // binds user (checking password) after looking-up attributes in BindDN context
err = bindUser(l, userDN, passwd) err = bindUser(l, userDN, passwd)
@ -440,17 +399,16 @@ func (source *Source) SearchEntry(name, passwd string, directBind bool) *SearchR
} }
return &SearchResult{ return &SearchResult{
LowerName: strings.ToLower(username), LowerName: strings.ToLower(username),
Username: username, Username: username,
Name: firstname, Name: firstname,
Surname: surname, Surname: surname,
Mail: mail, Mail: mail,
SSHPublicKey: sshPublicKey, SSHPublicKey: sshPublicKey,
IsAdmin: isAdmin, IsAdmin: isAdmin,
IsRestricted: isRestricted, IsRestricted: isRestricted,
Avatar: Avatar, Avatar: Avatar,
LdapTeamAdd: teamsToAdd, Groups: usersLdapGroups,
LdapTeamRemove: teamsToRemove,
} }
} }
@ -512,33 +470,29 @@ func (source *Source) SearchEntries() ([]*SearchResult, error) {
result := make([]*SearchResult, 0, len(sr.Entries)) result := make([]*SearchResult, 0, len(sr.Entries))
for _, v := range sr.Entries { for _, v := range sr.Entries {
teamsToAdd := make(map[string][]string) var usersLdapGroups container.Set[string]
teamsToRemove := make(map[string][]string)
if source.GroupsEnabled { if source.GroupsEnabled {
userAttributeListedInGroup := source.getUserAttributeListedInGroup(v) userAttributeListedInGroup := source.getUserAttributeListedInGroup(v)
if source.GroupFilter != "" { if source.GroupFilter != "" {
usersLdapGroups := source.listLdapGroupMemberships(l, userAttributeListedInGroup, true) usersLdapGroups = source.listLdapGroupMemberships(l, userAttributeListedInGroup, true)
if len(usersLdapGroups) == 0 { if len(usersLdapGroups) == 0 {
continue continue
} }
} }
if source.GroupTeamMap != "" || source.GroupTeamMapRemoval { if source.GroupTeamMap != "" || source.GroupTeamMapRemoval {
usersLdapGroups := source.listLdapGroupMemberships(l, userAttributeListedInGroup, false) usersLdapGroups = source.listLdapGroupMemberships(l, userAttributeListedInGroup, false)
teamsToAdd, teamsToRemove = source.getMappedMemberships(usersLdapGroups, userAttributeListedInGroup)
} }
} }
user := &SearchResult{ user := &SearchResult{
Username: v.GetAttributeValue(source.AttributeUsername), Username: v.GetAttributeValue(source.AttributeUsername),
Name: v.GetAttributeValue(source.AttributeName), Name: v.GetAttributeValue(source.AttributeName),
Surname: v.GetAttributeValue(source.AttributeSurname), Surname: v.GetAttributeValue(source.AttributeSurname),
Mail: v.GetAttributeValue(source.AttributeMail), Mail: v.GetAttributeValue(source.AttributeMail),
IsAdmin: checkAdmin(l, source, v.DN), IsAdmin: checkAdmin(l, source, v.DN),
LdapTeamAdd: teamsToAdd, Groups: usersLdapGroups,
LdapTeamRemove: teamsToRemove,
} }
if !user.IsAdmin { if !user.IsAdmin {

View File

@ -13,8 +13,10 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
auth_module "code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
source_service "code.gitea.io/gitea/services/auth/source"
user_service "code.gitea.io/gitea/services/user" user_service "code.gitea.io/gitea/services/user"
) )
@ -65,6 +67,11 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
orgCache := make(map[string]*organization.Organization) orgCache := make(map[string]*organization.Organization)
teamCache := make(map[string]*organization.Team) teamCache := make(map[string]*organization.Team)
groupTeamMapping, err := auth_module.UnmarshalGroupTeamMapping(source.GroupTeamMap)
if err != nil {
return err
}
for _, su := range sr { for _, su := range sr {
select { select {
case <-ctx.Done(): case <-ctx.Done():
@ -173,7 +180,9 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
} }
// Synchronize LDAP groups with organization and team memberships // Synchronize LDAP groups with organization and team memberships
if source.GroupsEnabled && (source.GroupTeamMap != "" || source.GroupTeamMapRemoval) { if source.GroupsEnabled && (source.GroupTeamMap != "" || source.GroupTeamMapRemoval) {
source.SyncLdapGroupsToTeams(usr, su.LdapTeamAdd, su.LdapTeamRemove, orgCache, teamCache) if err := source_service.SyncGroupsToTeamsCached(ctx, usr, su.Groups, groupTeamMapping, source.GroupTeamMapRemoval, orgCache, teamCache); err != nil {
log.Error("SyncGroupsToTeamsCached: %v", err)
}
} }
} }

View File

@ -8,13 +8,6 @@ import (
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
) )
// ________ _____ __ .__ ________
// \_____ \ / _ \ __ ___/ |_| |__ \_____ \
// / | \ / /_\ \| | \ __\ | \ / ____/
// / | \/ | \ | /| | | Y \/ \
// \_______ /\____|__ /____/ |__| |___| /\_______ \
// \/ \/ \/ \/
// Source holds configuration for the OAuth2 login source. // Source holds configuration for the OAuth2 login source.
type Source struct { type Source struct {
Provider string Provider string
@ -24,13 +17,15 @@ type Source struct {
CustomURLMapping *CustomURLMapping CustomURLMapping *CustomURLMapping
IconURL string IconURL string
Scopes []string Scopes []string
RequiredClaimName string RequiredClaimName string
RequiredClaimValue string RequiredClaimValue string
GroupClaimName string GroupClaimName string
AdminGroup string AdminGroup string
RestrictedGroup string GroupTeamMap string
SkipLocalTwoFA bool `json:",omitempty"` GroupTeamMapRemoval bool
RestrictedGroup string
SkipLocalTwoFA bool `json:",omitempty"`
// reference to the authSource // reference to the authSource
authSource *auth.Source authSource *auth.Source

View File

@ -0,0 +1,116 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package source
import (
"context"
"fmt"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/log"
)
type syncType int
const (
syncAdd syncType = iota
syncRemove
)
// SyncGroupsToTeams maps authentication source groups to organization and team memberships
func SyncGroupsToTeams(ctx context.Context, user *user_model.User, sourceUserGroups container.Set[string], sourceGroupTeamMapping map[string]map[string][]string, performRemoval bool) error {
orgCache := make(map[string]*organization.Organization)
teamCache := make(map[string]*organization.Team)
return SyncGroupsToTeamsCached(ctx, user, sourceUserGroups, sourceGroupTeamMapping, performRemoval, orgCache, teamCache)
}
// SyncGroupsToTeamsCached maps authentication source groups to organization and team memberships
func SyncGroupsToTeamsCached(ctx context.Context, user *user_model.User, sourceUserGroups container.Set[string], sourceGroupTeamMapping map[string]map[string][]string, performRemoval bool, orgCache map[string]*organization.Organization, teamCache map[string]*organization.Team) error {
membershipsToAdd, membershipsToRemove := resolveMappedMemberships(sourceUserGroups, sourceGroupTeamMapping)
if performRemoval {
if err := syncGroupsToTeamsCached(ctx, user, membershipsToRemove, syncRemove, orgCache, teamCache); err != nil {
return fmt.Errorf("could not sync[remove] user groups: %w", err)
}
}
if err := syncGroupsToTeamsCached(ctx, user, membershipsToAdd, syncAdd, orgCache, teamCache); err != nil {
return fmt.Errorf("could not sync[add] user groups: %w", err)
}
return nil
}
func resolveMappedMemberships(sourceUserGroups container.Set[string], sourceGroupTeamMapping map[string]map[string][]string) (map[string][]string, map[string][]string) {
membershipsToAdd := map[string][]string{}
membershipsToRemove := map[string][]string{}
for group, memberships := range sourceGroupTeamMapping {
isUserInGroup := sourceUserGroups.Contains(group)
if isUserInGroup {
for org, teams := range memberships {
membershipsToAdd[org] = teams
}
} else {
for org, teams := range memberships {
membershipsToRemove[org] = teams
}
}
}
return membershipsToAdd, membershipsToRemove
}
func syncGroupsToTeamsCached(ctx context.Context, user *user_model.User, orgTeamMap map[string][]string, action syncType, orgCache map[string]*organization.Organization, teamCache map[string]*organization.Team) error {
for orgName, teamNames := range orgTeamMap {
var err error
org, ok := orgCache[orgName]
if !ok {
org, err = organization.GetOrgByName(ctx, orgName)
if err != nil {
if organization.IsErrOrgNotExist(err) {
// organization must be created before group sync
log.Warn("group sync: Could not find organisation %s: %v", orgName, err)
continue
}
return err
}
orgCache[orgName] = org
}
for _, teamName := range teamNames {
team, ok := teamCache[orgName+teamName]
if !ok {
team, err = org.GetTeam(ctx, teamName)
if err != nil {
if organization.IsErrTeamNotExist(err) {
// team must be created before group sync
log.Warn("group sync: Could not find team %s: %v", teamName, err)
continue
}
return err
}
teamCache[orgName+teamName] = team
}
isMember, err := organization.IsTeamMember(ctx, org.ID, team.ID, user.ID)
if err != nil {
return err
}
if action == syncAdd && !isMember {
if err := models.AddTeamMember(team, user.ID); err != nil {
log.Error("group sync: Could not add user to team: %v", err)
return err
}
} else if action == syncRemove && isMember {
if err := models.RemoveTeamMember(team, user.ID); err != nil {
log.Error("group sync: Could not remove user from team: %v", err)
return err
}
}
}
}
return nil
}

View File

@ -72,13 +72,15 @@ type AuthenticationForm struct {
Oauth2GroupClaimName string Oauth2GroupClaimName string
Oauth2AdminGroup string Oauth2AdminGroup string
Oauth2RestrictedGroup string Oauth2RestrictedGroup string
Oauth2GroupTeamMap string `binding:"ValidGroupTeamMap"`
Oauth2GroupTeamMapRemoval bool
SkipLocalTwoFA bool SkipLocalTwoFA bool
SSPIAutoCreateUsers bool SSPIAutoCreateUsers bool
SSPIAutoActivateUsers bool SSPIAutoActivateUsers bool
SSPIStripDomainNames bool SSPIStripDomainNames bool
SSPISeparatorReplacement string `binding:"AlphaDashDot;MaxSize(5)"` SSPISeparatorReplacement string `binding:"AlphaDashDot;MaxSize(5)"`
SSPIDefaultLanguage string SSPIDefaultLanguage string
GroupTeamMap string GroupTeamMap string `binding:"ValidGroupTeamMap"`
GroupTeamMapRemoval bool GroupTeamMapRemoval bool
} }

View File

@ -98,6 +98,9 @@ func GetDefaultMergeMessage(ctx context.Context, baseGitRepo *git.Repository, pr
} }
for _, ref := range refs { for _, ref := range refs {
if ref.RefAction == references.XRefActionCloses { if ref.RefAction == references.XRefActionCloses {
if err := ref.LoadIssue(ctx); err != nil {
return "", "", err
}
closeIssueIndexes = append(closeIssueIndexes, fmt.Sprintf("%s %s%d", closeWord, issueReference, ref.Issue.Index)) closeIssueIndexes = append(closeIssueIndexes, fmt.Sprintf("%s %s%d", closeWord, issueReference, ref.Issue.Index))
} }
} }

View File

@ -361,6 +361,14 @@
<label for="oauth2_restricted_group">{{.locale.Tr "admin.auths.oauth2_restricted_group"}}</label> <label for="oauth2_restricted_group">{{.locale.Tr "admin.auths.oauth2_restricted_group"}}</label>
<input id="oauth2_restricted_group" name="oauth2_restricted_group" value="{{$cfg.RestrictedGroup}}"> <input id="oauth2_restricted_group" name="oauth2_restricted_group" value="{{$cfg.RestrictedGroup}}">
</div> </div>
<div class="field">
<label>{{.locale.Tr "admin.auths.oauth2_map_group_to_team"}}</label>
<input name="oauth2_group_team_map" value="{{$cfg.GroupTeamMap}}" placeholder='e.g. {"Developer": {"MyGiteaOrganization": ["MyGiteaTeam1", "MyGiteaTeam2"]}}'>
</div>
<div class="ui checkbox">
<label>{{.locale.Tr "admin.auths.oauth2_map_group_to_team_removal"}}</label>
<input name="oauth2_group_team_map_removal" type="checkbox" {{if $cfg.GroupTeamMapRemoval}}checked{{end}}>
</div>
{{end}} {{end}}
<!-- SSPI --> <!-- SSPI -->

View File

@ -52,7 +52,7 @@
</div> </div>
<div class="field"> <div class="field">
<label for="restricted_filter">{{.locale.Tr "admin.auths.restricted_filter"}}</label> <label for="restricted_filter">{{.locale.Tr "admin.auths.restricted_filter"}}</label>
<input id="restricted_filter" name="admin_filter" value="{{.restricted_filter}}"> <input id="restricted_filter" name="restricted_filter" value="{{.restricted_filter}}">
<p class="help">{{.locale.Tr "admin.auths.restricted_filter_helper"}}</p> <p class="help">{{.locale.Tr "admin.auths.restricted_filter_helper"}}</p>
</div> </div>
<div class="field"> <div class="field">

View File

@ -98,4 +98,12 @@
<label for="oauth2_restricted_group">{{.locale.Tr "admin.auths.oauth2_restricted_group"}}</label> <label for="oauth2_restricted_group">{{.locale.Tr "admin.auths.oauth2_restricted_group"}}</label>
<input id="oauth2_restricted_group" name="oauth2_restricted_group" value="{{.oauth2_group_claim_name}}"> <input id="oauth2_restricted_group" name="oauth2_restricted_group" value="{{.oauth2_group_claim_name}}">
</div> </div>
<div class="field">
<label>{{.locale.Tr "admin.auths.oauth2_map_group_to_team"}}</label>
<input name="oauth2_group_team_map" value="{{.group_team_map}}" placeholder='e.g. {"Developer": {"MyGiteaOrganization": ["MyGiteaTeam1", "MyGiteaTeam2"]}}'>
</div>
<div class="ui checkbox">
<label>{{.locale.Tr "admin.auths.oauth2_map_group_to_team_removal"}}</label>
<input name="oauth2_group_team_map_removal" type="checkbox" {{if .group_team_map_removal}}checked{{end}}>
</div>
</div> </div>

View File

@ -112,23 +112,14 @@ func getLDAPServerPort() string {
return port return port
} }
func addAuthSourceLDAP(t *testing.T, sshKeyAttribute, groupFilter string, groupMapParams ...string) { func buildAuthSourceLDAPPayload(csrf, sshKeyAttribute, groupFilter, groupTeamMap, groupTeamMapRemoval string) map[string]string {
groupTeamMapRemoval := "off"
groupTeamMap := ""
if len(groupMapParams) == 2 {
groupTeamMapRemoval = groupMapParams[0]
groupTeamMap = groupMapParams[1]
}
// Modify user filter to test group filter explicitly // Modify user filter to test group filter explicitly
userFilter := "(&(objectClass=inetOrgPerson)(memberOf=cn=git,ou=people,dc=planetexpress,dc=com)(uid=%s))" userFilter := "(&(objectClass=inetOrgPerson)(memberOf=cn=git,ou=people,dc=planetexpress,dc=com)(uid=%s))"
if groupFilter != "" { if groupFilter != "" {
userFilter = "(&(objectClass=inetOrgPerson)(uid=%s))" userFilter = "(&(objectClass=inetOrgPerson)(uid=%s))"
} }
session := loginUser(t, "user1") return map[string]string{
csrf := GetCSRF(t, session, "/admin/auths/new")
req := NewRequestWithValues(t, "POST", "/admin/auths/new", map[string]string{
"_csrf": csrf, "_csrf": csrf,
"type": "2", "type": "2",
"name": "ldap", "name": "ldap",
@ -154,7 +145,19 @@ func addAuthSourceLDAP(t *testing.T, sshKeyAttribute, groupFilter string, groupM
"group_team_map": groupTeamMap, "group_team_map": groupTeamMap,
"group_team_map_removal": groupTeamMapRemoval, "group_team_map_removal": groupTeamMapRemoval,
"user_uid": "DN", "user_uid": "DN",
}) }
}
func addAuthSourceLDAP(t *testing.T, sshKeyAttribute, groupFilter string, groupMapParams ...string) {
groupTeamMapRemoval := "off"
groupTeamMap := ""
if len(groupMapParams) == 2 {
groupTeamMapRemoval = groupMapParams[0]
groupTeamMap = groupMapParams[1]
}
session := loginUser(t, "user1")
csrf := GetCSRF(t, session, "/admin/auths/new")
req := NewRequestWithValues(t, "POST", "/admin/auths/new", buildAuthSourceLDAPPayload(csrf, sshKeyAttribute, groupFilter, groupTeamMap, groupTeamMapRemoval))
session.MakeRequest(t, req, http.StatusSeeOther) session.MakeRequest(t, req, http.StatusSeeOther)
} }
@ -202,26 +205,7 @@ func TestLDAPAuthChange(t *testing.T) {
binddn, _ := doc.Find(`input[name="bind_dn"]`).Attr("value") binddn, _ := doc.Find(`input[name="bind_dn"]`).Attr("value")
assert.Equal(t, binddn, "uid=gitea,ou=service,dc=planetexpress,dc=com") assert.Equal(t, binddn, "uid=gitea,ou=service,dc=planetexpress,dc=com")
req = NewRequestWithValues(t, "POST", href, map[string]string{ req = NewRequestWithValues(t, "POST", href, buildAuthSourceLDAPPayload(csrf, "", "", "", "off"))
"_csrf": csrf,
"type": "2",
"name": "ldap",
"host": getLDAPServerHost(),
"port": "389",
"bind_dn": "uid=gitea,ou=service,dc=planetexpress,dc=com",
"bind_password": "password",
"user_base": "ou=people,dc=planetexpress,dc=com",
"filter": "(&(objectClass=inetOrgPerson)(memberOf=cn=git,ou=people,dc=planetexpress,dc=com)(uid=%s))",
"admin_filter": "(memberOf=cn=admin_staff,ou=people,dc=planetexpress,dc=com)",
"restricted_filter": "(uid=leela)",
"attribute_username": "uid",
"attribute_name": "givenName",
"attribute_surname": "sn",
"attribute_mail": "mail",
"attribute_ssh_public_key": "",
"is_sync_enabled": "on",
"is_active": "on",
})
session.MakeRequest(t, req, http.StatusSeeOther) session.MakeRequest(t, req, http.StatusSeeOther)
req = NewRequest(t, "GET", href) req = NewRequest(t, "GET", href)
@ -395,7 +379,7 @@ func TestLDAPGroupTeamSyncAddMember(t *testing.T) {
} }
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()
addAuthSourceLDAP(t, "", "", "on", `{"cn=ship_crew,ou=people,dc=planetexpress,dc=com":{"org26": ["team11"]},"cn=admin_staff,ou=people,dc=planetexpress,dc=com": {"non-existent": ["non-existent"]}}`) addAuthSourceLDAP(t, "", "", "on", `{"cn=ship_crew,ou=people,dc=planetexpress,dc=com":{"org26": ["team11"]},"cn=admin_staff,ou=people,dc=planetexpress,dc=com": {"non-existent": ["non-existent"]}}`)
org, err := organization.GetOrgByName("org26") org, err := organization.GetOrgByName(db.DefaultContext, "org26")
assert.NoError(t, err) assert.NoError(t, err)
team, err := organization.GetTeam(db.DefaultContext, org.ID, "team11") team, err := organization.GetTeam(db.DefaultContext, org.ID, "team11")
assert.NoError(t, err) assert.NoError(t, err)
@ -440,7 +424,7 @@ func TestLDAPGroupTeamSyncRemoveMember(t *testing.T) {
} }
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()
addAuthSourceLDAP(t, "", "", "on", `{"cn=dispatch,ou=people,dc=planetexpress,dc=com": {"org26": ["team11"]}}`) addAuthSourceLDAP(t, "", "", "on", `{"cn=dispatch,ou=people,dc=planetexpress,dc=com": {"org26": ["team11"]}}`)
org, err := organization.GetOrgByName("org26") org, err := organization.GetOrgByName(db.DefaultContext, "org26")
assert.NoError(t, err) assert.NoError(t, err)
team, err := organization.GetTeam(db.DefaultContext, org.ID, "team11") team, err := organization.GetTeam(db.DefaultContext, org.ID, "team11")
assert.NoError(t, err) assert.NoError(t, err)
@ -468,24 +452,15 @@ func TestLDAPGroupTeamSyncRemoveMember(t *testing.T) {
assert.False(t, isMember, "User membership should have been removed from team") assert.False(t, isMember, "User membership should have been removed from team")
} }
// Login should work even if Team Group Map contains a broken JSON func TestLDAPPreventInvalidGroupTeamMap(t *testing.T) {
func TestBrokenLDAPMapUserSignin(t *testing.T) {
if skipLDAPTests() { if skipLDAPTests() {
t.Skip() t.Skip()
return return
} }
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()
addAuthSourceLDAP(t, "", "", "on", `{"NOT_A_VALID_JSON"["MISSING_DOUBLE_POINT"]}`)
u := gitLDAPUsers[0] session := loginUser(t, "user1")
csrf := GetCSRF(t, session, "/admin/auths/new")
session := loginUserWithPassword(t, u.UserName, u.Password) req := NewRequestWithValues(t, "POST", "/admin/auths/new", buildAuthSourceLDAPPayload(csrf, "", "", `{"NOT_A_VALID_JSON"["MISSING_DOUBLE_POINT"]}`, "off"))
req := NewRequest(t, "GET", "/user/settings") session.MakeRequest(t, req, http.StatusOK) // StatusOK = failed, StatusSeeOther = ok
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
assert.Equal(t, u.UserName, htmlDoc.GetInputValueByName("name"))
assert.Equal(t, u.FullName, htmlDoc.GetInputValueByName("full_name"))
assert.Equal(t, u.Email, htmlDoc.Find(`label[for="email"]`).Siblings().First().Text())
} }

View File

@ -20,7 +20,8 @@
<SvgIcon name="octicon-meter" class="ui text yellow" class-name="job-status-rotate" v-else-if="job.status === 'running'"/> <SvgIcon name="octicon-meter" class="ui text yellow" class-name="job-status-rotate" v-else-if="job.status === 'running'"/>
<SvgIcon name="octicon-x-circle-fill" class="red" v-else/> <SvgIcon name="octicon-x-circle-fill" class="red" v-else/>
{{ job.name }} {{ job.name }}
<button class="job-brief-rerun" @click="rerunJob(index)" v-if="job.canRerun"> <!-- TODO: it will be a better idea to move "button" out from "a", and the ".prevent" will not be needed. But it needs some work with CSS -->
<button class="job-brief-rerun" @click.prevent="rerunJob(index)" v-if="job.canRerun">
<SvgIcon name="octicon-sync" class="ui text black"/> <SvgIcon name="octicon-sync" class="ui text black"/>
</button> </button>
</a> </a>
@ -162,12 +163,14 @@ const sfc = {
} }
}, },
// rerun a job // rerun a job
rerunJob(idx) { async rerunJob(idx) {
this.fetch(`${this.run.link}/jobs/${idx}/rerun`); const jobLink = `${this.run.link}/jobs/${idx}`;
await this.fetchPost(`${jobLink}/rerun`);
window.location.href = jobLink;
}, },
// cancel a run // cancel a run
cancelRun() { cancelRun() {
this.fetch(`${this.run.link}/cancel`); this.fetchPost(`${this.run.link}/cancel`);
}, },
createLogLine(line) { createLogLine(line) {
@ -205,7 +208,7 @@ const sfc = {
// for example: make cursor=null means the first time to fetch logs, cursor=eof means no more logs, etc // for example: make cursor=null means the first time to fetch logs, cursor=eof means no more logs, etc
return {step: idx, cursor: it.cursor, expanded: it.expanded}; return {step: idx, cursor: it.cursor, expanded: it.expanded};
}); });
const resp = await this.fetch( const resp = await this.fetchPost(
`${this.actionsURL}/runs/${this.runIndex}/jobs/${this.jobIndex}`, `${this.actionsURL}/runs/${this.runIndex}/jobs/${this.jobIndex}`,
JSON.stringify({logCursors}), JSON.stringify({logCursors}),
); );
@ -245,7 +248,7 @@ const sfc = {
} }
}, },
fetch(url, body) { fetchPost(url, body) {
return fetch(url, { return fetch(url, {
method: 'POST', method: 'POST',
headers: { headers: {