Skip to content
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

Support instance-wide OAuth2 applications #21335

Merged
merged 10 commits into from
Oct 12, 2022
177 changes: 177 additions & 0 deletions routers/web/admin/applications.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.package admin
wxiaoguang marked this conversation as resolved.
Show resolved Hide resolved

package admin

import (
"fmt"
"net/http"

"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
)

var (
// tplSettingsLabels template path for render application settings
tplSettingsApplications base.TplName = "admin/applications/list"
// tplSettingsLabels template path for render application edit settings
tplSettingsEditApplication base.TplName = "admin/applications/edit"
)

const instanceOwnerUserID = 0

// Applications render admin applications page
func Applications(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings.applications")
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminApplications"] = true

apps, err := auth.GetOAuth2ApplicationsByUserID(ctx, instanceOwnerUserID)
if err != nil {
ctx.ServerError("GetOAuth2ApplicationsByUserID", err)
return
}
ctx.Data["Applications"] = apps

ctx.HTML(http.StatusOK, tplSettingsApplications)
}

// ApplicationsPost response for adding an oauth2 application
func ApplicationsPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.EditOAuth2ApplicationForm)
ctx.Data["Title"] = ctx.Tr("settings.applications")
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminApplications"] = true

if ctx.HasError() {
apps, err := auth.GetOAuth2ApplicationsByUserID(ctx, instanceOwnerUserID)
if err != nil {
ctx.ServerError("GetOAuth2ApplicationsByUserID", err)
return
}
ctx.Data["Applications"] = apps

ctx.HTML(http.StatusOK, tplSettingsApplications)
return
}

app, err := auth.CreateOAuth2Application(ctx, auth.CreateOAuth2ApplicationOptions{
Name: form.Name,
RedirectURIs: []string{form.RedirectURI},
UserID: instanceOwnerUserID,
})
if err != nil {
ctx.ServerError("CreateOAuth2Application", err)
return
}
ctx.Data["App"] = app
ctx.Data["ClientSecret"], err = app.GenerateClientSecret()
if err != nil {
ctx.ServerError("GenerateClientSecret", err)
return
}
ctx.Flash.Success(ctx.Tr("settings.create_oauth2_application_success"))
ctx.HTML(http.StatusOK, tplSettingsEditApplication)
}

// EditApplication response for editing oauth2 application
func EditApplication(ctx *context.Context) {
app, err := auth.GetOAuth2ApplicationByID(ctx, ctx.ParamsInt64("id"))
if err != nil {
if auth.IsErrOAuthApplicationNotFound(err) {
ctx.NotFound("Application not found", err)
return
}
ctx.ServerError("GetOAuth2ApplicationByID", err)
return
}
if app.UID != instanceOwnerUserID {
ctx.NotFound("Application not found", nil)
return
}
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminApplications"] = true
ctx.Data["App"] = app
ctx.HTML(http.StatusOK, tplSettingsEditApplication)
}

// EditApplicationPost response for editing oauth2 application
func EditApplicationPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.EditOAuth2ApplicationForm)
ctx.Data["Title"] = ctx.Tr("settings.applications")
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminApplications"] = true

if ctx.HasError() {
apps, err := auth.GetOAuth2ApplicationsByUserID(ctx, instanceOwnerUserID)
if err != nil {
ctx.ServerError("GetOAuth2ApplicationsByUserID", err)
return
}
ctx.Data["Applications"] = apps

ctx.HTML(http.StatusOK, tplSettingsApplications)
return
}
var err error
if ctx.Data["App"], err = auth.UpdateOAuth2Application(auth.UpdateOAuth2ApplicationOptions{
ID: ctx.ParamsInt64("id"),
Name: form.Name,
RedirectURIs: []string{form.RedirectURI},
UserID: instanceOwnerUserID,
}); err != nil {
ctx.ServerError("UpdateOAuth2Application", err)
return
}
ctx.Flash.Success(ctx.Tr("settings.update_oauth2_application_success"))
ctx.HTML(http.StatusOK, tplSettingsEditApplication)
}

// ApplicationsRegenerateSecret handles the post request for regenerating the secret
func ApplicationsRegenerateSecret(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsAdminApplications"] = true
ctx.Data["PageIsAdmin"] = true

app, err := auth.GetOAuth2ApplicationByID(ctx, ctx.ParamsInt64("id"))
if err != nil {
if auth.IsErrOAuthApplicationNotFound(err) {
ctx.NotFound("Application not found", err)
return
}
ctx.ServerError("GetOAuth2ApplicationByID", err)
return
}
if app.UID != instanceOwnerUserID {
ctx.NotFound("Application not found", nil)
return
}
ctx.Data["App"] = app
ctx.Data["ClientSecret"], err = app.GenerateClientSecret()
if err != nil {
ctx.ServerError("GenerateClientSecret", err)
return
}
ctx.Flash.Success(ctx.Tr("settings.update_oauth2_application_success"))
ctx.HTML(http.StatusOK, tplSettingsEditApplication)
}

// DeleteApplication deletes the given oauth2 application
func DeleteApplication(ctx *context.Context) {
if err := auth.DeleteOAuth2Application(ctx.FormInt64("id"), instanceOwnerUserID); err != nil {
ctx.ServerError("DeleteOAuth2Application", err)
return
}
log.Trace("OAuth2 Application deleted: %s", ctx.Doer.Name)

ctx.Flash.Success(ctx.Tr("settings.remove_oauth2_application_success"))
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": fmt.Sprintf("%s/admin/applications", setting.AppSubURL),
})
}
15 changes: 10 additions & 5 deletions routers/web/auth/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -380,10 +380,13 @@ func AuthorizeOAuth(ctx *context.Context) {
return
}

user, err := user_model.GetUserByID(app.UID)
if err != nil {
ctx.ServerError("GetUserByID", err)
return
var user *user_model.User
if app.UID != 0 {
user, err = user_model.GetUserByID(app.UID)
if err != nil {
ctx.ServerError("GetUserByID", err)
return
}
}

if !app.ContainsRedirectURI(form.RedirectURI) {
Expand Down Expand Up @@ -475,7 +478,9 @@ func AuthorizeOAuth(ctx *context.Context) {
ctx.Data["State"] = form.State
ctx.Data["Scope"] = form.Scope
ctx.Data["Nonce"] = form.Nonce
ctx.Data["ApplicationUserLinkHTML"] = "<a href=\"" + html.EscapeString(user.HTMLURL()) + "\">@" + html.EscapeString(user.Name) + "</a>"
if user != nil {
ctx.Data["ApplicationUserLinkHTML"] = "<a href=\"" + html.EscapeString(user.HTMLURL()) + "\">@" + html.EscapeString(user.Name) + "</a>"
}
ctx.Data["ApplicationRedirectDomainHTML"] = "<strong>" + html.EscapeString(form.RedirectURI) + "</strong>"
// TODO document SESSION <=> FORM
err = ctx.Session.Set("client_id", app.ClientID)
Expand Down
17 changes: 17 additions & 0 deletions routers/web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,23 @@ func RegisterRoutes(m *web.Route) {
m.Post("/delete", admin.DeleteNotices)
m.Post("/empty", admin.EmptyNotices)
})

m.Group("/applications", func() {
m.Combo("").Get(admin.Applications).
Post(bindIgnErr(forms.EditOAuth2ApplicationForm{}), admin.ApplicationsPost)
m.Group("/{id}", func() {
m.Combo("").Get(admin.EditApplication).Post(bindIgnErr(forms.EditOAuth2ApplicationForm{}), admin.EditApplicationPost)
m.Post("/regenerate_secret", admin.ApplicationsRegenerateSecret)
m.Post("/delete", admin.DeleteApplication)
})
}, func(ctx *context.Context) {
if !setting.OAuth2.Enable {
ctx.Error(http.StatusForbidden)
return
}
})
}, func(ctx *context.Context) {
ctx.Data["EnableOAuth2"] = setting.OAuth2.Enable
}, adminReq)
// ***** END: Admin *****

Expand Down
55 changes: 55 additions & 0 deletions templates/admin/applications/edit.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{{template "base/head" .}}
<div class="page-content admin config">
{{template "admin/navbar" .}}
<div class="ui container">
qwerty287 marked this conversation as resolved.
Show resolved Hide resolved
<div class="twelve wide column content">
{{template "base/alert" .}}
<h4 class="ui top attached header">
{{.locale.Tr "settings.edit_oauth2_application"}}
</h4>
<div class="ui attached segment form ignore-dirty">
{{.CsrfTokenHtml}}
<div class="field">
<label for="client-id">{{.locale.Tr "settings.oauth2_client_id"}}</label>
<input id="client-id" readonly value="{{.App.ClientID}}">
</div>
{{if .ClientSecret}}
<div class="field">
<label for="client-secret">{{.locale.Tr "settings.oauth2_client_secret"}}</label>
<input id="client-secret" type="text" readonly value="{{.ClientSecret}}">
</div>
{{else}}
<div class="field">
<label for="client-secret">{{.locale.Tr "settings.oauth2_client_secret"}}</label>
<input id="client-secret" type="password" readonly value="averysecuresecret">
</div>
{{end}}
<div class="item">
{{.locale.Tr "settings.oauth2_regenerate_secret_hint"}}
<form class="ui form ignore-dirty" action="{{AppSubUrl}}/admin/applications/{{.App.ID}}/regenerate_secret" method="post">
{{.CsrfTokenHtml}}
<a href="#" onclick="event.target.parentNode.submit()">{{.locale.Tr "settings.oauth2_regenerate_secret"}}</a>
</form>
</div>
</div>
<div class="ui attached bottom segment">
<form class="ui form ignore-dirty" action="{{AppSubUrl}}/admin/applications/{{.App.ID}}" method="post">
{{.CsrfTokenHtml}}
<div class="field {{if .Err_AppName}}error{{end}}">
<label for="application-name">{{.locale.Tr "settings.oauth2_application_name"}}</label>
<input id="application-name" value="{{.App.Name}}" name="application_name" required>
</div>
<div class="field {{if .Err_RedirectURI}}error{{end}}">
<label for="redirect-uri">{{.locale.Tr "settings.oauth2_redirect_uri"}}</label>
<input type="url" name="redirect_uri" value="{{.App.PrimaryRedirectURI}}" id="redirect-uri">
</div>
<button class="ui green button">
{{.locale.Tr "settings.save_application"}}
</button>
</form>
</div>
</div>

</div>
</div>
{{template "base/footer" .}}
69 changes: 69 additions & 0 deletions templates/admin/applications/list.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{{template "base/head" .}}
<div class="page-content admin config">
{{template "admin/navbar" .}}
<div class="ui container">
<div class="twelve wide column content">
{{template "base/alert" .}}
<h4 class="ui top attached header">
{{.locale.Tr "settings.applications"}}
</h4>
<div class="ui attached segment">
<div class="ui key list">
<div class="item">
{{.locale.Tr "settings.oauth2_application_create_description"}}
</div>
{{range $app := .Applications}}
<div class="item">
<div class="right floated content">
<a href="{{$.Link}}/{{$app.ID}}" class="ui primary tiny button">
{{svg "octicon-pencil" 16 "mr-2"}}
{{$.locale.Tr "settings.oauth2_application_edit"}}
</a>
<button class="ui red tiny button delete-button" data-modal-id="remove-gitea-oauth2-application"
data-url="{{$.Link}}/{{.ID}}/delete"
data-id="{{$app.ID}}">
{{svg "octicon-trash" 16 "mr-2"}}
{{$.locale.Tr "settings.delete_key"}}
</button>
</div>
<div class="content">
<strong>{{$app.Name}}</strong>
</div>
</div>
{{end}}
</div>
<div class="ui attached bottom segment">
<h5 class="ui top header">
{{.locale.Tr "settings.create_oauth2_application"}}
</h5>
<form class="ui form ignore-dirty" action="{{.Link}}" method="post">
{{.CsrfTokenHtml}}
<div class="field {{if .Err_AppName}}error{{end}}">
<label for="application-name">{{.locale.Tr "settings.oauth2_application_name"}}</label>
<input id="application-name" name="application_name" value="{{.application_name}}" required>
</div>
<div class="field {{if .Err_RedirectURI}}error{{end}}">
<label for="redirect-uri">{{.locale.Tr "settings.oauth2_redirect_uri"}}</label>
<input type="url" name="redirect_uri" id="redirect-uri">
</div>
<button class="ui green button">
{{.locale.Tr "settings.create_oauth2_application_button"}}
</button>
</form>
</div>

<div class="ui small basic delete modal" id="remove-gitea-oauth2-application">
<div class="ui icon header">
{{svg "octicon-trash"}}
{{.locale.Tr "settings.remove_oauth2_application"}}
</div>
<div class="content">
<p>{{.locale.Tr "settings.oauth2_application_remove_description"}}</p>
</div>
{{template "base/delete_modal_actions" .}}
</div>
</div>
</div>
</div>
</div>
{{template "base/footer" .}}
5 changes: 5 additions & 0 deletions templates/admin/navbar.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@
<a class="{{if .PageIsAdminEmails}}active{{end}} item" href="{{AppSubUrl}}/admin/emails">
{{.locale.Tr "admin.emails"}}
</a>
{{if .EnableOAuth2}}
<a class="{{if .PageIsAdminApplications}}active{{end}} item" href="{{AppSubUrl}}/admin/applications">
{{.locale.Tr "settings.applications"}}
</a>
{{end}}
<a class="{{if .PageIsAdminConfig}}active{{end}} item" href="{{AppSubUrl}}/admin/config">
{{.locale.Tr "admin.config"}}
</a>
Expand Down
2 changes: 1 addition & 1 deletion templates/user/auth/grant.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
{{template "base/alert" .}}
<p>
<b>{{.locale.Tr "auth.authorize_application_description"}}</b><br/>
{{.locale.Tr "auth.authorize_application_created_by" .ApplicationUserLinkHTML | Str2html}}
{{if .ApplicationUserLinkHTML}}{{.locale.Tr "auth.authorize_application_created_by" .ApplicationUserLinkHTML | Str2html}}{{end}}
</p>
</div>
<div class="ui attached segment">
Expand Down