-
-
Notifications
You must be signed in to change notification settings - Fork 5.8k
Add api support for external authentication management #34234
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 12 commits
17bd1e0
c2f5544
c830bc1
25425ad
91247d6
62dc4c2
c3fc57b
4787ea4
cb0e0ce
c31df25
cf3d746
6a27fbe
0646881
2de3030
8377386
f4ab93c
cb7b359
983e648
098de09
b58f94a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
// Copyright 2015 The Gogs Authors. All rights reserved. | ||
// Copyright 2019 The Gitea Authors. All rights reserved. | ||
// SPDX-License-Identifier: MIT | ||
|
||
package structs | ||
|
||
type AuthOauth2Option struct { | ||
ID int64 `json:"id"` | ||
AuthenticationName string `json:"authentication_name" binding:"Required"` | ||
Type int `json:"type"` | ||
TypeName string `json:"type_name"` | ||
|
||
IsActive bool `json:"is_active"` | ||
IsSyncEnabled bool `json:"is_sync_enabled"` | ||
} | ||
|
||
// CreateUserOption create user options | ||
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 | ||
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"` | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,283 @@ | ||
// Copyright 2015 The Gogs Authors. All rights reserved. | ||
// Copyright 2019 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) { | ||
uvulpos marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// 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: generateScopes(), | ||
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) { | ||
uvulpos marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// 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.CreateAuthOauth2Option) | ||
|
||
config := &oauth2.Source{ | ||
Provider: "openidConnect", | ||
ClientID: form.ProviderClientID, | ||
ClientSecret: form.ProviderClientSecret, | ||
OpenIDConnectAutoDiscoveryURL: form.ProviderAutoDiscoveryURL, | ||
CustomURLMapping: nil, | ||
IconURL: form.ProviderIconURL, | ||
Scopes: generateScopes(), | ||
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) { | ||
uvulpos marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// 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) { | ||
uvulpos marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// 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{}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Filter and return just type OAuth There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure search is needed? Second opinion would be nice this though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The url explicit says oauth, so I'm not sure if I want to receive LDAP connections There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To filter for oauth only you should add But I was wondering why oauth search specifically instead of more generic one? The struct doesn't really let you filter by much more too. |
||
if err != nil { | ||
ctx.APIErrorInternal(err) | ||
return | ||
} | ||
|
||
results := make([]*api.AuthOauth2Option, 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) | ||
} | ||
|
||
// ??? todo: what should I do here? | ||
func generateScopes() []string { | ||
var scopes []string | ||
|
||
// for _, s := range strings.Split(form.Oauth2Scopes, ",") { | ||
// s = strings.TrimSpace(s) | ||
// if s != "" { | ||
// scopes = append(scopes, s) | ||
// } | ||
// } | ||
|
||
return scopes | ||
} | ||
uvulpos marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1657,6 +1657,15 @@ func Routes() *web.Router { | |
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(false, true), reqToken(), reqTeamMembership(), checkTokenPublicOnly()) | ||
|
||
m.Group("/admin", func() { | ||
m.Group("/identity-auth", func() { | ||
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) | ||
Comment on lines
+1670
to
+1671
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. parameter via url or url param? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unless someone else has different opinion I think it should stay like this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Ok, I'll add this feature to get a config by ID |
||
}) | ||
}) | ||
|
||
m.Group("/cron", func() { | ||
m.Get("", admin.ListCronTasks) | ||
m.Post("/{task}", admin.PostCronTask) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure why end user would be interested in numeric type?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was not sure, but I think it's more unique than the name for machine readability. But... I don't know. Opinions are welcome :)