diff --git a/.github/workflows/build-api.yml b/.github/workflows/build-api.yml index 120f6bb1b..05484597c 100644 --- a/.github/workflows/build-api.yml +++ b/.github/workflows/build-api.yml @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Prepare input branch - if: ${{ github.event.inputs.branch != "" }} + if: ${{ github.event.inputs.branch != '' }} run: echo "branch=refs/heads/${{ github.event.inputs.branch }}" >> $GITHUB_ENV - name: Checkout Repository @@ -70,9 +70,9 @@ jobs: TAGS+=",$name:latest" echo "TAGS=$TAGS" >> $GITHUB_ENV - - name: Set the tag on wokflow dispatch + - name: Set the tag on workflow dispatch if: ${{ github.ref_type != 'tag' }} - run: echo "TAGS=$(git rev-parse --short HEAD)" >> $GITHUB_ENV + run: echo "TAGS=voltaserve/api:$(git rev-parse --short HEAD)" >> $GITHUB_ENV - name: Build and Push Docker Image uses: docker/build-push-action@v5 diff --git a/.github/workflows/build-conversion.yml b/.github/workflows/build-conversion.yml index 81f07696e..6d6beee23 100644 --- a/.github/workflows/build-conversion.yml +++ b/.github/workflows/build-conversion.yml @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Prepare input branch - if: ${{ github.event.inputs.branch != "" }} + if: ${{ github.event.inputs.branch != '' }} run: echo "branch=refs/heads/${{ github.event.inputs.branch }}" >> $GITHUB_ENV - name: Checkout Repository @@ -70,9 +70,9 @@ jobs: TAGS+=",$name:latest" echo "TAGS=$TAGS" >> $GITHUB_ENV - - name: Set the tag on wokflow dispatch + - name: Set the tag on workflow dispatch if: ${{ github.ref_type != 'tag' }} - run: echo "TAGS=$(git rev-parse --short HEAD)" >> $GITHUB_ENV + run: echo "TAGS=voltaserve/conversion:$(git rev-parse --short HEAD)" >> $GITHUB_ENV - name: Build and Push Docker Image uses: docker/build-push-action@v5 diff --git a/.github/workflows/build-idp.yml b/.github/workflows/build-idp.yml index 274feca4d..ec4bab0d1 100644 --- a/.github/workflows/build-idp.yml +++ b/.github/workflows/build-idp.yml @@ -70,7 +70,7 @@ jobs: TAGS+=",$name:latest" echo "TAGS=$TAGS" >> $GITHUB_ENV - - name: Set the tag on wokflow dispatch + - name: Set the tag on workflow dispatch if: ${{ github.ref_type != 'tag' }} run: echo "TAGS=$(git rev-parse --short HEAD)" >> $GITHUB_ENV diff --git a/.github/workflows/build-language.yml b/.github/workflows/build-language.yml index 0d5e400e3..8a0893958 100644 --- a/.github/workflows/build-language.yml +++ b/.github/workflows/build-language.yml @@ -75,7 +75,7 @@ jobs: TAGS+=",$name:latest" echo "TAGS=$TAGS" >> $GITHUB_ENV - - name: Set the tag on wokflow dispatch + - name: Set the tag on workflow dispatch if: ${{ github.ref_type != 'tag' }} run: echo "TAGS=$(git rev-parse --short HEAD)" >> $GITHUB_ENV diff --git a/.github/workflows/build-mosaic.yml b/.github/workflows/build-mosaic.yml index b7fcaf63a..b8cb33cb6 100644 --- a/.github/workflows/build-mosaic.yml +++ b/.github/workflows/build-mosaic.yml @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Prepare input branch - if: ${{ github.event.inputs.branch != "" }} + if: ${{ github.event.inputs.branch != '' }} run: echo "branch=refs/heads/${{ github.event.inputs.branch }}" >> $GITHUB_ENV - name: Checkout Repository @@ -70,9 +70,9 @@ jobs: TAGS+=",$name:latest" echo "TAGS=$TAGS" >> $GITHUB_ENV - - name: Set the tag on wokflow dispatch + - name: Set the tag on workflow dispatch if: ${{ github.ref_type != 'tag' }} - run: echo "TAGS=$(git rev-parse --short HEAD)" >> $GITHUB_ENV + run: echo "TAGS=voltaserve/mosaic:$(git rev-parse --short HEAD)" >> $GITHUB_ENV - name: Build and Push Docker Image uses: docker/build-push-action@v5 diff --git a/.github/workflows/build-ui.yml b/.github/workflows/build-ui.yml index 8492b7275..e1c3bf04c 100644 --- a/.github/workflows/build-ui.yml +++ b/.github/workflows/build-ui.yml @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Prepare input branch - if: ${{ github.event.inputs.branch != "" }} + if: ${{ github.event.inputs.branch != '' }} run: echo "branch=refs/heads/${{ github.event.inputs.branch }}" >> $GITHUB_ENV - name: Checkout Repository @@ -70,9 +70,9 @@ jobs: TAGS+=",$name:latest" echo "TAGS=$TAGS" >> $GITHUB_ENV - - name: Set the tag on wokflow dispatch + - name: Set the tag on workflow dispatch if: ${{ github.ref_type != 'tag' }} - run: echo "TAGS=$(git rev-parse --short HEAD)" >> $GITHUB_ENV + run: echo "TAGS=voltaserve/ui:$(git rev-parse --short HEAD)" >> $GITHUB_ENV - name: Build and Push Docker Image uses: docker/build-push-action@v5 diff --git a/.github/workflows/build-webdav.yml b/.github/workflows/build-webdav.yml index 1319bc30c..63aebc467 100644 --- a/.github/workflows/build-webdav.yml +++ b/.github/workflows/build-webdav.yml @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Prepare input branch - if: ${{ github.event.inputs.branch != "" }} + if: ${{ github.event.inputs.branch != '' }} run: echo "branch=refs/heads/${{ github.event.inputs.branch }}" >> $GITHUB_ENV - name: Checkout Repository @@ -70,9 +70,9 @@ jobs: TAGS+=",$name:latest" echo "TAGS=$TAGS" >> $GITHUB_ENV - - name: Set the tag on wokflow dispatch + - name: Set the tag on workflow dispatch if: ${{ github.ref_type != 'tag' }} - run: echo "TAGS=$(git rev-parse --short HEAD)" >> $GITHUB_ENV + run: echo "TAGS=voltaserve/webdav:$(git rev-parse --short HEAD)" >> $GITHUB_ENV - name: Build and Push Docker Image uses: docker/build-push-action@v5 diff --git a/api/.golangci.yml b/api/.golangci.yml index 7fc36c952..ee37a56bb 100644 --- a/api/.golangci.yml +++ b/api/.golangci.yml @@ -39,7 +39,6 @@ linters: - prealloc - nestif - lll - - gosec - gocritic - gocognit - funlen diff --git a/api/infra/fixtures/join-organization.eml b/api/infra/fixtures/join-organization.eml new file mode 100644 index 000000000..fe058b835 --- /dev/null +++ b/api/infra/fixtures/join-organization.eml @@ -0,0 +1,64 @@ +Mime-Version: 1.0 +Date: Now +From: "Voltaserve" +To: "Someone" +Subject: Invitation to join a Voltaserve organization +Content-Type: multipart/alternative; + boundary=XXX + +--XXX +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain ; charset=UTF-8 + +You have been invited by Someone to join the organization ACME. +Please follow this link to sign up: example.com/sign-up +--XXX +Content-Transfer-Encoding: quoted-printable +Content-Type: text/html; charset=UTF-8 + + + + + + + + + +
+

Hello,

+

+ You have been invited by Someone to join the + organization ACME. Please follow this link to + view your incoming invitations: +

+

+ + View incoming invitations + +

+
+ + + +--XXX diff --git a/api/infra/fixtures/sign-up-and-join-organization.eml b/api/infra/fixtures/sign-up-and-join-organization.eml new file mode 100644 index 000000000..663e5dba9 --- /dev/null +++ b/api/infra/fixtures/sign-up-and-join-organization.eml @@ -0,0 +1,63 @@ +Mime-Version: 1.0 +Date: Now +From: "Voltaserve" +To: "Someone" +Subject: Invitation to join a Voltaserve organization +Content-Type: multipart/alternative; + boundary=XXX + +--XXX +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain ; charset=UTF-8 + +You have been invited by Someone to join the organization ACME. +Please follow this link to view your incoming invitations: example.com/acco= +unt/incoming-invitations +--XXX +Content-Transfer-Encoding: quoted-printable +Content-Type: text/html; charset=UTF-8 + + + + + + + + + +
+

Hello,

+

+ You have been invited by Someone to join the + organization ACME in Voltaserve. Please follow + this link to sign up: +

+

+ Sign up +

+
+ + + +--XXX diff --git a/api/infra/mail.go b/api/infra/mail.go index 4e8c37e0c..c49329548 100644 --- a/api/infra/mail.go +++ b/api/infra/mail.go @@ -14,7 +14,7 @@ import ( "bytes" "fmt" "io" - "os" + "io/fs" "path/filepath" "text/template" @@ -23,30 +23,39 @@ import ( "github.com/kouprlabs/voltaserve/api/config" "github.com/kouprlabs/voltaserve/api/log" + "github.com/kouprlabs/voltaserve/api/templates" ) +type dialer interface { + DialAndSend(m ...*gomail.Message) error +} + type MessageParams struct { Subject string } type MailTemplate struct { - dialer *gomail.Dialer + dialer dialer config config.SMTPConfig } -func NewMailTemplate() *MailTemplate { - mt := new(MailTemplate) - mt.config = config.GetConfig().SMTP - mt.dialer = gomail.NewDialer(mt.config.Host, mt.config.Port, mt.config.Username, mt.config.Password) - return mt +func NewMailTemplate(config config.SMTPConfig) *MailTemplate { + return NewMailTemplateWithDialer(config, gomail.NewDialer(config.Host, config.Port, config.Username, config.Password)) +} + +func NewMailTemplateWithDialer(config config.SMTPConfig, dialer dialer) *MailTemplate { + return &MailTemplate{ + config: config, + dialer: dialer, + } } func (mt *MailTemplate) Send(templateName string, address string, variables map[string]string) error { - html, err := mt.getText(filepath.FromSlash("templates/"+templateName+"/template.html"), variables) + html, err := mt.getText(filepath.FromSlash(templateName+"/template.html"), variables) if err != nil { return err } - text, err := mt.getText(filepath.FromSlash("templates/"+templateName+"/template.txt"), variables) + text, err := mt.getText(filepath.FromSlash(templateName+"/template.txt"), variables) if err != nil { return err } @@ -61,17 +70,17 @@ func (mt *MailTemplate) Send(templateName string, address string, variables map[ m.SetBody("text/plain ", text) m.AddAlternative("text/html", html) if err := mt.dialer.DialAndSend(m); err != nil { - return err + return fmt.Errorf("dial and sending mail: %w", err) } return nil } func (mt *MailTemplate) getText(path string, variables map[string]string) (string, error) { - f, err := os.Open(path) + f, err := templates.FS.Open(path) if err != nil { return "", err } - defer func(f *os.File) { + defer func(f fs.File) { if err := f.Close(); err != nil { log.GetLogger().Error(err) } @@ -91,11 +100,11 @@ func (mt *MailTemplate) getText(path string, variables map[string]string) (strin } func (mt *MailTemplate) getMessageParams(templateName string) (*MessageParams, error) { - f, err := os.Open(filepath.FromSlash("templates/" + templateName + "/params.yml")) + f, err := templates.FS.Open(filepath.FromSlash(templateName + "/params.yml")) if err != nil { return nil, err } - defer func(f *os.File) { + defer func(f fs.File) { if err := f.Close(); err != nil { log.GetLogger().Error(err) } diff --git a/api/infra/mail_test.go b/api/infra/mail_test.go new file mode 100644 index 000000000..5694fd251 --- /dev/null +++ b/api/infra/mail_test.go @@ -0,0 +1,101 @@ +package infra_test + +import ( + _ "embed" + "fmt" + "regexp" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/gomail.v2" + + "github.com/kouprlabs/voltaserve/api/config" + "github.com/kouprlabs/voltaserve/api/infra" +) + +type DialMock struct { + Err error + Body string +} + +func (d *DialMock) DialAndSend(m ...*gomail.Message) error { + var body strings.Builder + + for _, message := range m { + _, err := message.WriteTo(&body) + if err != nil { + return fmt.Errorf("write to: %w", err) + } + } + + d.Body = body.String() + + return d.Err +} + +//go:embed fixtures/join-organization.eml +var joinOrganization string + +//go:embed fixtures/sign-up-and-join-organization.eml +var signupAndJoinOrganization string + +func TestMailTemplate_Send(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + TemplateName string + Address string + Variables map[string]string + ExpectedBody string + }{ + "join-organization": { + TemplateName: "join-organization", + Address: `"Someone" `, + Variables: map[string]string{ + "USER_FULL_NAME": "Someone", + "ORGANIZATION_NAME": "ACME", + "UI_URL": "example.com", + }, + ExpectedBody: joinOrganization, + }, + "signup-and-join-organization": { + TemplateName: "signup-and-join-organization", + Address: `"Someone" `, + Variables: map[string]string{ + "USER_FULL_NAME": "Someone", + "ORGANIZATION_NAME": "ACME", + "UI_URL": "example.com", + }, + ExpectedBody: signupAndJoinOrganization, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + dialMock := &DialMock{} + + mt := infra.NewMailTemplateWithDialer(config.SMTPConfig{ + SenderName: "Voltaserve", + SenderAddress: "voltaserve@example.com", + }, dialMock) + + // gomail is non-deterministic in its headers, so we'll brute force our expected body. + assert.EventuallyWithT(t, func(t *assert.CollectT) { + err := mt.Send(tc.TemplateName, tc.Address, tc.Variables) + require.NoError(t, err) + + simplifiedBody := regexp.MustCompile("boundary=.+").ReplaceAllString(dialMock.Body, "boundary=XXX") + simplifiedBody = regexp.MustCompile("--.+(|--)").ReplaceAllString(simplifiedBody, "--XXX$1") + simplifiedBody = regexp.MustCompile("Date: .+").ReplaceAllString(simplifiedBody, "Date: Now") + simplifiedBody = strings.ReplaceAll(simplifiedBody, "\r\n", "\n") + + assert.Equal(t, tc.ExpectedBody, simplifiedBody) + }, 1*time.Second, 1) + }) + } +} diff --git a/api/service/invitation_service.go b/api/service/invitation_service.go index 138a1278e..83c8dbba0 100644 --- a/api/service/invitation_service.go +++ b/api/service/invitation_service.go @@ -44,7 +44,7 @@ func NewInvitationService() *InvitationService { invitationRepo: repo.NewInvitationRepo(), invitationMapper: newInvitationMapper(), userRepo: repo.NewUserRepo(), - mailTmpl: infra.NewMailTemplate(), + mailTmpl: infra.NewMailTemplate(config.GetConfig().SMTP), orgMapper: newOrganizationMapper(), config: config.GetConfig(), } diff --git a/api/templates/templates.go b/api/templates/templates.go new file mode 100644 index 000000000..dec2588b6 --- /dev/null +++ b/api/templates/templates.go @@ -0,0 +1,6 @@ +package templates + +import "embed" + +//go:embed * +var FS embed.FS diff --git a/conversion/.golangci.yml b/conversion/.golangci.yml index 0da178343..df380653a 100644 --- a/conversion/.golangci.yml +++ b/conversion/.golangci.yml @@ -35,7 +35,6 @@ linters: - prealloc - nestif - lll - - gosec - gocritic - gocognit - funlen diff --git a/conversion/processor/pdf_processor.go b/conversion/processor/pdf_processor.go index a35c6183f..74eb45f70 100644 --- a/conversion/processor/pdf_processor.go +++ b/conversion/processor/pdf_processor.go @@ -11,7 +11,9 @@ package processor import ( + "errors" "fmt" + "io/fs" "os" "path/filepath" "strconv" @@ -37,22 +39,25 @@ func NewPDFProcessor() *PDFProcessor { } func (p *PDFProcessor) TextFromPDF(inputPath string) (*string, error) { - tmpPath := filepath.FromSlash(os.TempDir() + "/" + helper.NewID() + ".txt") + tmpPath := filepath.Join(os.TempDir(), helper.NewID()+".txt") + if err := infra.NewCommand().Exec("pdftotext", inputPath, tmpPath); err != nil { return nil, err } + defer func(path string) { - _, err := os.Stat(path) - if os.IsExist(err) { - if err := os.Remove(path); err != nil { - infra.GetLogger().Error(err) - } + if err := os.Remove(path); errors.Is(err, fs.ErrNotExist) { + return + } else if err != nil { + infra.GetLogger().Error(err) } }(tmpPath) - b, err := os.ReadFile(tmpPath) + + b, err := os.ReadFile(tmpPath) //nolint:gosec // Known path if err != nil { return nil, err } + return helper.ToPtr(strings.TrimSpace(string(b))), nil } diff --git a/mosaic/.golangci.yml b/mosaic/.golangci.yml index efe6206fa..0ee40b13d 100644 --- a/mosaic/.golangci.yml +++ b/mosaic/.golangci.yml @@ -28,7 +28,6 @@ linters: - cyclop - prealloc - lll - - gosec - gocritic - funlen - gocognit diff --git a/mosaic/builder/mosaic_builder.go b/mosaic/builder/mosaic_builder.go index 37a0b3745..077c60dae 100644 --- a/mosaic/builder/mosaic_builder.go +++ b/mosaic/builder/mosaic_builder.go @@ -86,7 +86,7 @@ func (mb *MosaicBuilder) SetActionOnExistingDirectory(action string) { func (mb *MosaicBuilder) Build() (*Metadata, error) { cleanupIfFails := false if _, err := os.Stat(mb.options.OutputDirectory); os.IsNotExist(err) { - if err := os.MkdirAll(mb.options.OutputDirectory, os.ModePerm); err != nil { + if err := os.MkdirAll(mb.options.OutputDirectory, 0o750); err != nil { return nil, err } cleanupIfFails = true @@ -133,7 +133,7 @@ func (mb *MosaicBuilder) Build() (*Metadata, error) { metadataFilePath := mb.GetMetadataFilePath() metadataBytes, _ := json.MarshalIndent(metadata, "", " ") - if err := os.WriteFile(metadataFilePath, metadataBytes, os.ModePerm); err != nil { + if err := os.WriteFile(metadataFilePath, metadataBytes, 0o600); err != nil { return nil, err } @@ -363,7 +363,7 @@ func (mb *MosaicBuilder) CreateDirectory(directory string) { return } } - if err := os.MkdirAll(directory, os.ModePerm); err != nil { + if err := os.MkdirAll(directory, 0o750); err != nil { return } } diff --git a/webdav/.golangci.yml b/webdav/.golangci.yml index 235f51c42..814d2a7fd 100644 --- a/webdav/.golangci.yml +++ b/webdav/.golangci.yml @@ -31,7 +31,6 @@ linters: - nestif - canonicalheader - lll - - gosec - funlen severity: diff --git a/webdav/client/api_client.go b/webdav/client/api_client.go index 4ab05fa73..e5f327ec2 100644 --- a/webdav/client/api_client.go +++ b/webdav/client/api_client.go @@ -435,7 +435,7 @@ func (cl *APIClient) DownloadOriginal(file *File, outputPath string) error { infra.GetLogger().Error(err.Error()) } }(resp.Body) - out, err := os.Create(outputPath) + out, err := os.Create(outputPath) //nolint:gosec // Known safe value if err != nil { return err } diff --git a/webdav/config/config.go b/webdav/config/config.go index 966bc9b06..7590b84dd 100644 --- a/webdav/config/config.go +++ b/webdav/config/config.go @@ -16,7 +16,8 @@ import ( ) type Config struct { - Port int + Host string + Port string APIURL string IdPURL string S3 S3Config @@ -46,12 +47,8 @@ var config *Config func GetConfig() *Config { if config == nil { - port, err := strconv.Atoi(os.Getenv("PORT")) - if err != nil { - panic(err) - } config = &Config{ - Port: port, + Port: os.Getenv("PORT"), } readURLs(config) readS3(config) diff --git a/webdav/handler/method_get.go b/webdav/handler/method_get.go index 24f9a28c5..ca6eba840 100644 --- a/webdav/handler/method_get.go +++ b/webdav/handler/method_get.go @@ -81,7 +81,7 @@ func (h *Handler) methodGet(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Length", fmt.Sprintf("%d", chunkSize)) w.Header().Set("Content-Type", "application/octet-stream") w.WriteHeader(http.StatusPartialContent) - file, err := os.Open(outputPath) + file, err := os.Open(outputPath) //nolint:gosec // Known safe path if err != nil { infra.HandleError(err, w) return @@ -106,7 +106,7 @@ func (h *Handler) methodGet(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size())) w.Header().Set("Content-Type", "application/octet-stream") w.WriteHeader(http.StatusOK) - file, err := os.Open(outputPath) + file, err := os.Open(outputPath) //nolint:gosec // Known safe path if err != nil { infra.HandleError(err, w) return diff --git a/webdav/handler/method_put.go b/webdav/handler/method_put.go index 907b83d59..b8614da7c 100644 --- a/webdav/handler/method_put.go +++ b/webdav/handler/method_put.go @@ -57,7 +57,7 @@ func (h *Handler) methodPut(w http.ResponseWriter, r *http.Request) { return } outputPath := filepath.Join(os.TempDir(), uuid.New().String()) - ws, err := os.Create(outputPath) + ws, err := os.Create(outputPath) //nolint:gosec // Known safe path if err != nil { infra.HandleError(err, w) return diff --git a/webdav/main.go b/webdav/main.go index ae3f5f9cc..d068725c1 100644 --- a/webdav/main.go +++ b/webdav/main.go @@ -12,8 +12,8 @@ package main import ( "context" - "fmt" "log" + "net" "net/http" "os" "strings" @@ -115,12 +115,19 @@ func main() { startTokenRefresh(idpClient) - log.Printf("Listening on port %d", cfg.Port) - log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", cfg.Port), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if strings.HasPrefix(r.URL.Path, "/v2/health") { - mux.ServeHTTP(w, r) - } else { - basicAuthMiddleware(mux, idpClient).ServeHTTP(w, r) - } - }))) + server := &http.Server{ + Addr: net.JoinHostPort(cfg.Host, cfg.Port), + ReadHeaderTimeout: 30 * time.Second, + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasPrefix(r.URL.Path, "/v2/health") { + mux.ServeHTTP(w, r) + } else { + basicAuthMiddleware(mux, idpClient).ServeHTTP(w, r) + } + }), + } + + log.Printf("Listening on %s", server.Addr) + + log.Fatal(server.ListenAndServe()) }