0
0
mirror of https://github.com/go-gitea/gitea.git synced 2025-12-08 15:45:27 +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

@ -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": [