Skip to content
Merged
2 changes: 0 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,6 @@ jobs:
PROJECT_KEY: etherpad-go

- name: SonarQube Quality Gate Check
# Temporarily continue on error to avoid blocking PRs
continue-on-error: true
uses: sonarsource/sonarqube-quality-gate-action@master
with:
scanMetadataReportFile: .scannerwork/report-task.txt
Expand Down
2 changes: 1 addition & 1 deletion assets/pad/pad.templ
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ templ ChatBox(translations map[string]string) {



templ Greeting(pad padModel.Model, jsScript string, translations map[string]string, settings settings.Settings) {
templ Greeting(pad padModel.Model, jsScript string, translations map[string]string, settings *settings.Settings) {
<html class="pad super-light-toolbar super-light-editor light-background">
<head>
<title>{settings.Title}</title>
Expand Down
2 changes: 1 addition & 1 deletion assets/welcome/main.templ
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"strconv"
)

templ Page(settings settings.Settings, translations map[string]string) {
templ Page(settings *settings.Settings, translations map[string]string) {
<html lang="en">
<head>
<script>
Expand Down
46 changes: 13 additions & 33 deletions lib/api/author/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package author
import (
"encoding/json"

error2 "github.com/ether/etherpad-go/lib/api/error"
"github.com/ether/etherpad-go/lib/api/errors"
"github.com/ether/etherpad-go/lib/author"
"github.com/ether/etherpad-go/lib/db"
"github.com/go-playground/validator/v10"
Expand All @@ -25,19 +25,17 @@ func Init(c *fiber.App, db db.DataStore, validator *validator.Validate) {
var dto CreateDto
err := json.Unmarshal(c.Body(), &dto)
if err != nil {
return c.Status(400).JSON(error2.Error{
Message: "Invalid request " + err.Error(),
Error: 400,
})
return c.Status(400).JSON(errors.InvalidRequestError)
}
err = validator.Struct(dto)
if err != nil {
return c.Status(400).JSON(error2.Error{
Message: "Validation error: " + err.Error(),
})
return c.Status(400).JSON(errors.NewInvalidParamError(err.Error()))
}

var createdAuthor = authorManager.CreateAuthor(&dto.Name)
createdAuthor, err := authorManager.CreateAuthor(&dto.Name)
if err != nil {
return c.Status(500).JSON(errors.InternalServerError)
}
return c.JSON(CreateDtoResponse{
AuthorId: createdAuthor.Id,
})
Expand All @@ -46,49 +44,31 @@ func Init(c *fiber.App, db db.DataStore, validator *validator.Validate) {
c.Get("/author/:authorId", func(c *fiber.Ctx) error {
var authorId = c.Params("authorId")
if authorId == "" {
return c.Status(400).JSON(error2.Error{
Message: "authorId is required",
Error: 400,
})
return c.Status(400).JSON(errors.NewInvalidParamError("authorId is required"))
}
var foundAuthor, err = authorManager.GetAuthor(authorId)
if foundAuthor == nil {
return c.Status(404).JSON(error2.Error{
Message: "Author not found",
Error: 404,
})
return c.Status(404).JSON(errors.AuthorNotFoundError)
}

if err != nil {
return c.Status(500).JSON(error2.Error{
Message: "Internal server error",
Error: 500,
})
return c.Status(500).JSON(errors.InternalServerError)
}

return c.JSON(foundAuthor)
})
c.Get("/author/:authorId/pads", func(c *fiber.Ctx) error {
var authorId = c.Params("authorId")
if authorId == "" {
return c.Status(400).JSON(error2.Error{
Message: "authorId is required",
Error: 400,
})
return c.Status(400).JSON(errors.NewInvalidParamError("authorId is required"))
}
var foundAuthor, err = authorManager.GetAuthor(authorId)
if foundAuthor == nil {
return c.Status(404).JSON(error2.Error{
Message: "Author not found",
Error: 404,
})
return c.Status(404).JSON(errors.AuthorNotFoundError)
}

if err != nil {
return c.Status(500).JSON(error2.Error{
Message: "Internal server error",
Error: 500,
})
return c.Status(500).JSON(errors.InternalServerError)
}

return c.JSON(foundAuthor.PadIDs)
Expand Down
11 changes: 11 additions & 0 deletions lib/api/constants/apiContentTypes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package constants

const (
ContentTypeHTML = "text/html; charset=utf-8"
ContentTypeJSON = "application/json"
ContentTypeXML = "application/xml"
ContentTypeFormData = "application/x-www-form-urlencoded"
ContentTypeTextPlain = "text/plain; charset=utf-8"
ContentTypeCSS = "text/css; charset=utf-8"
ContentTypeJS = "application/javascript; charset=utf-8"
)
2 changes: 1 addition & 1 deletion lib/api/error/APIError.go → lib/api/errors/APIError.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package error
package errors

type Error struct {
Message string `json:"message"`
Expand Down
90 changes: 90 additions & 0 deletions lib/api/errors/httpErrors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package errors

var InternalApiError = Error{
Message: "Internal API Error",
Error: 1,
}

var InvalidRevisionError = Error{
Message: "Invalid revision number",
Error: 400,
}

var RevisionHigherThanHeadError = Error{
Message: "Revision number is higher than head",
Error: 400,
}

var InvalidRequestError = Error{
Message: "Invalid request",
Error: 400,
}

func NewInvalidParamError(paramName string) Error {
return Error{
Message: "Invalid parameter: " + paramName,
Error: 400,
}
}

func NewMissingParamError(paramName string) Error {
return Error{
Message: "Missing parameter: " + paramName,
Error: 400,
}
}

var PadNotFoundError = Error{
Message: "Pad not found",
Error: 404,
}

var AuthorNotFoundError = Error{
Message: "Author not found",
Error: 404,
}

var RevisionNotFoundError = Error{
Message: "Revision not found",
Error: 404,
}

var InternalServerError = Error{
Message: "Internal server error",
Error: 500,
}

var TextConversionError = Error{
Message: "Failed to convert text",
Error: 500,
}

var DataRetrievalError = Error{
Message: "Failed to retrieve data",
Error: 500,
}

var UnauthorizedError = Error{
Message: "Unauthorized access",
Error: 401,
}

var ForbiddenError = Error{
Message: "Access forbidden",
Error: 403,
}

var PadAlreadyExistsError = Error{
Message: "Pad already exists",
Error: 409,
}

var ValidationError = Error{
Message: "Validation failed",
Error: 422,
}

var InvalidParameterError = Error{
Message: "Invalid parameter provided",
Error: 422,
}
7 changes: 2 additions & 5 deletions lib/api/groups/init.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
package groups

import (
error2 "github.com/ether/etherpad-go/lib/api/error"
"github.com/ether/etherpad-go/lib/api/errors"
"github.com/gofiber/fiber/v2"
)

func Init(app *fiber.App) {
app.Get("/groups/pads", func(c *fiber.Ctx) error {
var groupId = c.Query("groupID")
if groupId == "" {
return c.Status(400).JSON(error2.Error{
Message: "groupID is required",
Error: 400,
})
return c.Status(400).JSON(errors.NewMissingParamError("groupID"))
}
return c.SendStatus(200)
})
Expand Down
2 changes: 1 addition & 1 deletion lib/api/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
"go.uber.org/zap"
)

func InitAPI(c *fiber.App, uiAssets embed.FS, retrievedSettings settings.Settings, cookieStore *session.Store, store db.DataStore, handler *ws.PadMessageHandler, manager *pad2.Manager, validator *validator.Validate, setupLogger *zap.SugaredLogger) {
func InitAPI(c *fiber.App, uiAssets embed.FS, retrievedSettings *settings.Settings, cookieStore *session.Store, store db.DataStore, handler *ws.PadMessageHandler, manager *pad2.Manager, validator *validator.Validate, setupLogger *zap.SugaredLogger) {
locales.Init(uiAssets)
author.Init(c, store, validator)
pad.Init(c, handler, manager)
Expand Down
80 changes: 44 additions & 36 deletions lib/api/oidc/authenticator.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,19 @@ import (
"log"
"math/big"
"net/http"
"slices"
"time"

"github.com/ether/etherpad-go/assets/login"
"github.com/ether/etherpad-go/lib/api/constants"
"github.com/ether/etherpad-go/lib/models/oidc"
"github.com/ether/etherpad-go/lib/settings"
"github.com/ory/fosite"
"github.com/ory/fosite/compose"
"github.com/ory/fosite/handler/openid"
"github.com/ory/fosite/token/jwt"
"go.uber.org/zap"
"golang.org/x/crypto/bcrypt"
)

type Authenticator struct {
Expand All @@ -30,16 +33,32 @@ type Authenticator struct {
func NewAuthenticator(retrievedSettings *settings.Settings) *Authenticator {
store := NewMemoryStore()
for _, sso := range retrievedSettings.SSO.Clients {
store.Clients[sso.ClientId] = &fosite.DefaultClient{
isPublic := false

if !slices.Contains(sso.GrantTypes, "client_credentials") {
isPublic = true
}

clientToSso := &fosite.DefaultClient{
ID: sso.ClientId,
Secret: []byte(sso.ClientSecret),
RedirectURIs: sso.RedirectUris,
GrantTypes: sso.GrantTypes,
Audience: []string{"etherpad-go"},
Public: true,
Public: isPublic,
ResponseTypes: []string{"code"},
Scopes: []string{"openid", "email", "profile", "offline"},
}

if sso.ClientSecret != nil && *sso.ClientSecret != "" {
hashedSecret, err := bcrypt.GenerateFromPassword([]byte(*sso.ClientSecret), bcrypt.DefaultCost)
if err != nil {
log.Fatalf("Error hashing client secret: %v", err)
}

clientToSso.Secret = hashedSecret
}
store.Clients[sso.ClientId] = clientToSso

}

for username, user := range retrievedSettings.Users {
Expand Down Expand Up @@ -181,7 +200,26 @@ func (a *Authenticator) OicWellKnown(rw http.ResponseWriter, req *http.Request,
rw.Write(byteResponse)
}

func (a *Authenticator) AuthEndpoint(rw http.ResponseWriter, req *http.Request, setupLogger *zap.SugaredLogger, retrievedSettings settings.Settings) {
func renderLoginPage(rw http.ResponseWriter, req *http.Request, clients []settings.SSOClient, ar fosite.AuthorizeRequester, errorMessage *string) {
clientId := req.URL.Query().Get("client_id")
var clientFound settings.SSOClient

for _, sso := range clients {
if sso.ClientId == clientId {
clientFound = sso
}
}

scopes := make([]string, 0)
for _, scope := range ar.GetRequestedScopes() {
scopes = append(scopes, scope)
}
loginComp := login.Login(clientFound, scopes, errorMessage)
req.Header.Set("Content-Type", constants.ContentTypeHTML)
loginComp.Render(req.Context(), rw)
}

func (a *Authenticator) AuthEndpoint(rw http.ResponseWriter, req *http.Request, setupLogger *zap.SugaredLogger, retrievedSettings *settings.Settings) {
ctx := req.Context()

ar, err := a.provider.NewAuthorizeRequest(ctx, req)
Expand All @@ -193,27 +231,11 @@ func (a *Authenticator) AuthEndpoint(rw http.ResponseWriter, req *http.Request,

req.ParseForm()
if req.Method == "GET" {
clientId := req.URL.Query().Get("client_id")
var clientFound settings.SSOClient

for _, sso := range retrievedSettings.SSO.Clients {
if sso.ClientId == clientId {
clientFound = sso
}
}

rw.Header().Set("Content-Type", "text/html; charset=utf-8")
scopes := make([]string, 0)
for _, scope := range ar.GetRequestedScopes() {
scopes = append(scopes, scope)
}
loginComp := login.Login(clientFound, scopes, nil)
loginComp.Render(req.Context(), rw)
renderLoginPage(rw, req, retrievedSettings.SSO.Clients, ar, nil)
return
}

for _, scope := range req.PostForm["scopes"] {
println(scope)
ar.GrantScope(scope)
}

Expand All @@ -225,22 +247,8 @@ func (a *Authenticator) AuthEndpoint(rw http.ResponseWriter, req *http.Request,
if !ok || user.Password != password {
time.Sleep(500 * time.Millisecond)
rw.WriteHeader(http.StatusOK)
var clientFound settings.SSOClient

for _, sso := range retrievedSettings.SSO.Clients {
if sso.ClientId == clientId {
clientFound = sso
}
}

rw.Header().Set("Content-Type", "text/html; charset=utf-8")
usernameOrPasswordInvalid := "Username or password invalid"
scopes := make([]string, 0)
for _, scope := range ar.GetRequestedScopes() {
scopes = append(scopes, scope)
}
loginComp := login.Login(clientFound, scopes, &usernameOrPasswordInvalid)
loginComp.Render(req.Context(), rw)
renderLoginPage(rw, req, retrievedSettings.SSO.Clients, ar, &usernameOrPasswordInvalid)
return
}

Expand Down
Loading