0
0
mirror of https://github.com/go-gitea/gitea.git synced 2025-12-08 11:25:21 +01:00

Merge 9025d69ace33ec474cd408cb86f555610254a83d into c287a8cdb589172bbba8969357a671dabc6596bd

This commit is contained in:
Tim Riedl 2025-12-06 14:23:42 +01:00 committed by GitHub
commit 91ebc78c16
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 655 additions and 3 deletions

View File

@ -1334,4 +1334,4 @@
"path": "xorm.io/xorm/LICENSE",
"licenseText": "Copyright (c) 2013 - 2015 The Xorm Authors\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\n* Neither the name of the {organization} nor the names of its\n contributors may be used to endorse or promote products derived from\n this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
}
]
]

View File

@ -240,13 +240,18 @@ func CreateSource(ctx context.Context, source *Source) error {
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 {
if err := DeleteSource(ctx, source.ID); err != nil {
log.Error("CreateSource: Error while wrapOpenIDConnectInitializeError: %v", err)
}
}
return err
}
func DeleteSource(ctx context.Context, id int64) error {
_, err := db.GetEngine(ctx).ID(id).Delete(new(Source))
return err
}
type FindSourcesOptions struct {
db.ListOptions
IsActive optional.Option[bool]

14
modules/structs/auth.go Normal file
View File

@ -0,0 +1,14 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package structs
// swagger:model
type AuthSourceOption struct {
ID int64 `json:"id"`
AuthenticationName string `json:"authentication_name" binding:"Required"`
TypeName string `json:"type_name"`
IsActive bool `json:"is_active"`
IsSyncEnabled bool `json:"is_sync_enabled"`
}

View File

@ -0,0 +1,52 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package structs
// CreateUserOption create user options
// swagger:model
type CreateAuthOauth2Option struct {
AuthenticationName string `json:"authentication_name" binding:"Required"`
ProviderIconURL string `json:"provider_icon_url"`
ProviderClientID string `json:"provider_client_id" binding:"Required"`
ProviderClientSecret string `json:"provider_client_secret" binding:"Required"`
ProviderAutoDiscoveryURL string `json:"provider_auto_discovery_url" binding:"Required"`
SkipLocal2FA bool `json:"skip_local_2fa"`
AdditionalScopes string `json:"additional_scopes"`
RequiredClaimName string `json:"required_claim_name"`
RequiredClaimValue string `json:"required_claim_value"`
ClaimNameProvidingGroupNameForSource string `json:"claim_name_providingGroupNameForSource"`
GroupClaimValueForAdministratorUsers string `json:"group_claim_value_for_administrator_users"`
GroupClaimValueForRestrictedUsers string `json:"group_claim_value_for_restricted_users"`
MapClaimedGroupsToOrganizationTeams string `json:"map_claimed_groups_to_organization_teams"`
RemoveUsersFromSyncronizedTeams bool `json:"RemoveUsersFromSyncronizedTeams"`
EnableUserSyncronization bool `json:"EnableUserSyncronization"`
AuthenticationSourceIsActive bool `json:"AuthenticationSourceIsActive"`
}
// EditUserOption edit user options
// swagger:model
type EditAuthOauth2Option struct {
AuthenticationName string `json:"authentication_name" binding:"Required"`
ProviderIconURL string `json:"provider_icon_url"`
ProviderClientID string `json:"provider_client_id" binding:"Required"`
ProviderClientSecret string `json:"provider_client_secret" binding:"Required"`
ProviderAutoDiscoveryURL string `json:"provider_auto_discovery_url" binding:"Required"`
SkipLocal2FA bool `json:"skip_local_2fa"`
AdditionalScopes string `json:"additional_scopes"`
RequiredClaimName string `json:"required_claim_name"`
RequiredClaimValue string `json:"required_claim_value"`
ClaimNameProvidingGroupNameForSource string `json:"claim_name_providingGroupNameForSource"`
GroupClaimValueForAdministratorUsers string `json:"group_claim_value_for_administrator_users"`
GroupClaimValueForRestrictedUsers string `json:"group_claim_value_for_restricted_users"`
MapClaimedGroupsToOrganizationTeams string `json:"map_claimed_groups_to_organization_teams"`
RemoveUsersFromSyncronizedTeams bool `json:"RemoveUsersFromSyncronizedTeams"`
EnableUserSyncronization bool `json:"EnableUserSyncronization"`
AuthenticationSourceIsActive bool `json:"AuthenticationSourceIsActive"`
}

View File

@ -0,0 +1,59 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package admin
import (
"net/http"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
// SearchAuth API for getting information of the configured authentication methods according the filter conditions
func SearchAuth(ctx *context.APIContext) {
// swagger:operation GET /admin/identity-auth admin adminSearchAuth
// ---
// summary: Search authentication sources
// produces:
// - application/json
// parameters:
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// description: "SearchResults of authentication sources"
// schema:
// type: array
// items:
// "$ref": "#/definitions/AuthOauth2Option"
// "403":
// "$ref": "#/responses/forbidden"
listOptions := utils.GetListOptions(ctx)
authSources, maxResults, err := db.FindAndCount[auth_model.Source](ctx, auth_model.FindSourcesOptions{})
if err != nil {
ctx.APIErrorInternal(err)
return
}
results := make([]*api.AuthSourceOption, len(authSources))
for i := range authSources {
results[i] = convert.ToOauthProvider(ctx, authSources[i])
}
ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, &results)
}

View File

@ -0,0 +1,270 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package admin
import (
"fmt"
"net/http"
"net/url"
"strconv"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/auth/source/oauth2"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
// CreateOauthAuth create a new external authentication for oauth2
func CreateOauthAuth(ctx *context.APIContext) {
// swagger:operation PUT /admin/identity-auth/oauth admin adminCreateOauth2Auth
// ---
// summary: Create an OAuth2 authentication source
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: body
// in: body
// required: true
// schema:
// "$ref": "#/definitions/CreateAuthOauth2Option"
// responses:
// "201":
// description: OAuth2 authentication source created successfully
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.CreateAuthOauth2Option)
discoveryURL, err := url.Parse(form.ProviderAutoDiscoveryURL)
if err != nil || (discoveryURL.Scheme != "http" && discoveryURL.Scheme != "https") {
_ = fmt.Errorf("invalid Auto Discovery URL: %s (this must be a valid URL starting with http:// or https://)", form.ProviderAutoDiscoveryURL)
ctx.HTTPError(http.StatusBadRequest, fmt.Sprintf("invalid Auto Discovery URL: %s (this must be a valid URL starting with http:// or https://)", form.ProviderAutoDiscoveryURL))
}
config := &oauth2.Source{
Provider: "openidConnect",
ClientID: form.ProviderClientID,
ClientSecret: form.ProviderClientSecret,
OpenIDConnectAutoDiscoveryURL: form.ProviderAutoDiscoveryURL,
CustomURLMapping: nil,
IconURL: form.ProviderIconURL,
Scopes: []string{},
RequiredClaimName: form.RequiredClaimName,
RequiredClaimValue: form.RequiredClaimValue,
SkipLocalTwoFA: form.SkipLocal2FA,
GroupClaimName: form.ClaimNameProvidingGroupNameForSource,
RestrictedGroup: form.GroupClaimValueForRestrictedUsers,
AdminGroup: form.GroupClaimValueForAdministratorUsers,
GroupTeamMap: form.MapClaimedGroupsToOrganizationTeams,
GroupTeamMapRemoval: form.RemoveUsersFromSyncronizedTeams,
}
createErr := auth_model.CreateSource(ctx, &auth_model.Source{
Type: auth_model.OAuth2,
Name: form.AuthenticationName,
IsActive: true,
Cfg: config,
})
if createErr != nil {
ctx.APIErrorInternal(createErr)
return
}
ctx.Status(http.StatusCreated)
}
// EditOauthAuth api for modifying a authentication method
func EditOauthAuth(ctx *context.APIContext) {
// swagger:operation PATCH /admin/identity-auth/oauth/{id} admin adminEditOauth2Auth
// ---
// summary: Update an OAuth2 authentication source
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: id
// in: path
// description: authentication source ID
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// required: true
// schema:
// "$ref": "#/definitions/CreateAuthOauth2Option"
// responses:
// "201":
// description: OAuth2 authentication source updated successfully
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
oauthIDString := ctx.PathParam("id")
oauthID, oauthIDErr := strconv.Atoi(oauthIDString)
if oauthIDErr != nil {
ctx.APIErrorInternal(oauthIDErr)
}
source, sourceErr := auth_model.GetSourceByID(ctx, int64(oauthID))
if sourceErr != nil {
ctx.APIErrorInternal(sourceErr)
return
}
if source.Type != auth_model.OAuth2 {
ctx.APIErrorNotFound()
return
}
form := web.GetForm(ctx).(*api.EditAuthOauth2Option)
config := &oauth2.Source{
Provider: "openidConnect",
ClientID: form.ProviderClientID,
ClientSecret: form.ProviderClientSecret,
OpenIDConnectAutoDiscoveryURL: form.ProviderAutoDiscoveryURL,
CustomURLMapping: nil,
IconURL: form.ProviderIconURL,
Scopes: []string{},
RequiredClaimName: form.RequiredClaimName,
RequiredClaimValue: form.RequiredClaimValue,
SkipLocalTwoFA: form.SkipLocal2FA,
GroupClaimName: form.ClaimNameProvidingGroupNameForSource,
RestrictedGroup: form.GroupClaimValueForRestrictedUsers,
AdminGroup: form.GroupClaimValueForAdministratorUsers,
GroupTeamMap: form.MapClaimedGroupsToOrganizationTeams,
GroupTeamMapRemoval: form.RemoveUsersFromSyncronizedTeams,
}
updateErr := auth_model.UpdateSource(ctx, &auth_model.Source{
ID: int64(oauthID),
Type: auth_model.OAuth2,
Name: form.AuthenticationName,
IsActive: true,
Cfg: config,
})
if updateErr != nil {
ctx.APIErrorInternal(updateErr)
return
}
ctx.Status(http.StatusCreated)
}
// DeleteOauthAuth api for deleting a authentication method
func DeleteOauthAuth(ctx *context.APIContext) {
// swagger:operation DELETE /admin/identity-auth/oauth/{id} admin adminDeleteOauth2Auth
// ---
// summary: Delete an OAuth2 authentication source
// produces:
// - application/json
// parameters:
// - name: id
// in: path
// description: authentication source ID
// type: integer
// format: int64
// required: true
// responses:
// "200":
// description: OAuth2 authentication source deleted successfully
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
oauthIDString := ctx.PathParam("id")
oauthID, oauthIDErr := strconv.Atoi(oauthIDString)
if oauthIDErr != nil {
ctx.APIErrorInternal(oauthIDErr)
}
source, sourceErr := auth_model.GetSourceByID(ctx, int64(oauthID))
if sourceErr != nil {
ctx.APIErrorInternal(sourceErr)
return
}
if source.Type != auth_model.OAuth2 {
ctx.APIErrorNotFound()
return
}
err := auth_model.DeleteSource(ctx, int64(oauthID))
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusOK)
}
// SearchOauthAuth API for getting information of the configured authentication methods according the filter conditions
func SearchOauthAuth(ctx *context.APIContext) {
// swagger:operation GET /admin/identity-auth/oauth admin adminSearchOauth2Auth
// ---
// summary: Search OAuth2 authentication sources
// produces:
// - application/json
// parameters:
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// description: "SearchResults of OAuth2 authentication sources"
// schema:
// type: array
// items:
// "$ref": "#/definitions/AuthOauth2Option"
// "403":
// "$ref": "#/responses/forbidden"
listOptions := utils.GetListOptions(ctx)
authSources, maxResults, err := db.FindAndCount[auth_model.Source](ctx, auth_model.FindSourcesOptions{
LoginType: auth_model.OAuth2,
})
if err != nil {
ctx.APIErrorInternal(err)
return
}
results := make([]*api.AuthSourceOption, len(authSources))
for i := range authSources {
results[i] = convert.ToOauthProvider(ctx, authSources[i])
}
ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, &results)
}

View File

@ -1696,6 +1696,16 @@ func Routes() *web.Router {
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(false, true), reqToken(), reqTeamMembership(), checkTokenPublicOnly())
m.Group("/admin", func() {
m.Group("/identity-auth", func() {
m.Get("", admin.SearchAuth)
m.Group("/oauth", func() {
m.Get("", admin.SearchOauthAuth)
m.Put("", bind(api.CreateAuthOauth2Option{}), admin.CreateOauthAuth)
m.Patch("/{id}", bind(api.EditAuthOauth2Option{}), admin.EditOauthAuth)
m.Delete("/{id}", admin.DeleteOauthAuth)
})
})
m.Group("/cron", func() {
m.Get("", admin.ListCronTasks)
m.Post("/{task}", admin.PostCronTask)

View File

@ -27,6 +27,7 @@ type Source struct {
GroupTeamMap string
GroupTeamMapRemoval bool
RestrictedGroup string
SkipLocalTwoFA bool
SSHPublicKeyClaimName string
FullNameClaimName string

View File

@ -0,0 +1,40 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package convert
import (
"context"
auth_model "code.gitea.io/gitea/models/auth"
api "code.gitea.io/gitea/modules/structs"
)
// ToOauthProvider convert auth_model.Source≤ to api.AuthOauth2Option
func ToOauthProvider(ctx context.Context, provider *auth_model.Source) *api.AuthSourceOption {
if provider == nil {
return nil
}
return toOauthProvider(provider)
}
// ToOauthProviders convert list of auth_model.Source to list of api.AuthOauth2Option
func ToOauthProviders(ctx context.Context, provider []*auth_model.Source) []*api.AuthSourceOption {
result := make([]*api.AuthSourceOption, len(provider))
for i := range provider {
result[i] = ToOauthProvider(ctx, provider[i])
}
return result
}
func toOauthProvider(provider *auth_model.Source) *api.AuthSourceOption {
return &api.AuthSourceOption{
ID: provider.ID,
AuthenticationName: provider.Name,
TypeName: provider.Type.String(),
IsActive: provider.IsActive,
IsSyncEnabled: provider.IsSyncEnabled,
}
}

View File

@ -21,7 +21,7 @@
},
"version": "{{.SwaggerAppVer}}"
},
"basePath": "{{.SwaggerAppSubUrl}}/api/v1",
"basePath": "/{{.SwaggerAppSubUrl}}/api/v1",
"paths": {
"/activitypub/user-id/{user-id}": {
"get": {
@ -582,6 +582,207 @@
}
}
},
"/admin/identity-auth": {
"get": {
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "Search authentication sources",
"operationId": "adminSearchAuth",
"parameters": [
{
"type": "integer",
"description": "page number of results to return (1-based)",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "page size of results",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"description": "SearchResults of authentication sources",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/AuthOauth2Option"
}
}
},
"403": {
"$ref": "#/responses/forbidden"
}
}
}
},
"/admin/identity-auth/oauth": {
"get": {
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "Search OAuth2 authentication sources",
"operationId": "adminSearchOauth2Auth",
"parameters": [
{
"type": "integer",
"description": "page number of results to return (1-based)",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "page size of results",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"description": "SearchResults of OAuth2 authentication sources",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/AuthOauth2Option"
}
}
},
"403": {
"$ref": "#/responses/forbidden"
}
}
},
"put": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "Create an OAuth2 authentication source",
"operationId": "adminCreateOauth2Auth",
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/CreateAuthOauth2Option"
}
}
],
"responses": {
"201": {
"description": "OAuth2 authentication source created successfully"
},
"400": {
"$ref": "#/responses/error"
},
"403": {
"$ref": "#/responses/forbidden"
},
"422": {
"$ref": "#/responses/validationError"
}
}
}
},
"/admin/identity-auth/oauth/{id}": {
"delete": {
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "Delete an OAuth2 authentication source",
"operationId": "adminDeleteOauth2Auth",
"parameters": [
{
"type": "integer",
"format": "int64",
"description": "authentication source ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OAuth2 authentication source deleted successfully"
},
"403": {
"$ref": "#/responses/forbidden"
},
"404": {
"$ref": "#/responses/notFound"
},
"422": {
"$ref": "#/responses/validationError"
}
}
},
"patch": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "Update an OAuth2 authentication source",
"operationId": "adminEditOauth2Auth",
"parameters": [
{
"type": "integer",
"format": "int64",
"description": "authentication source ID",
"name": "id",
"in": "path",
"required": true
},
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/CreateAuthOauth2Option"
}
}
],
"responses": {
"201": {
"description": "OAuth2 authentication source updated successfully"
},
"400": {
"$ref": "#/responses/error"
},
"403": {
"$ref": "#/responses/forbidden"
},
"404": {
"$ref": "#/responses/notFound"
},
"422": {
"$ref": "#/responses/validationError"
}
}
}
},
"/admin/orgs": {
"get": {
"produces": [