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

Draft : feat(GIST-78): added ABAC on gists #25

Merged
merged 5 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .github/workflows/cov-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Continuous testing with coverage
on:
workflow_dispatch:
schedule:
- cron: "0 12,23 * * *"

jobs:
covtest:
name: Integration tests with coverage
runs-on: ubuntu-latest
services:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: gists
POSTGRES_USER: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- name: Install Go toolchain
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
with:
go-version: 1.23.0
- uses: extractions/setup-just@v2
- name: Run migrations
run: go build -o main && chmod +x ./main && PORT="4000" PG_USER="postgres" PG_PASSWORD="postgres" PG_PORT="5432" PG_HOST="0.0.0.0" PG_DATABASE="gists" GOOGLE_KEY="" GOOGLE_SECRET="" GITHUB_KEY="" GITHUB_SECRET="" PUBLIC_URL="http://localhost:4000" APP_KEY="DUMP_APP_KEY_FOR_TEST" SMTP_HOST="" MAIL_SMTP="" MAIL_PASSWORD="" SMTP_PORT="" FRONTEND_URL="http://localhost:3000" ./main migrate
- name: Run tests
run: |
export PORT="4000" PG_USER="postgres" PG_PASSWORD="postgres" PG_PORT="5432" PG_HOST="0.0.0.0" PG_DATABASE="gists" GOOGLE_KEY="" GOOGLE_SECRET="" GITHUB_KEY="" GITHUB_SECRET="" PUBLIC_URL="http://localhost:4000" APP_KEY="DUMP_APP_KEY_FOR_TEST" SMTP_HOST="" MAIL_SMTP="" MAIL_PASSWORD="" SMTP_PORT="" FRONTEND_URL="http://localhost:3000"
just report-all
- uses: actions/upload-artifact@v4
with:
name: coverage-result
path: ./test/coverage/
2 changes: 1 addition & 1 deletion .github/workflows/mr-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ jobs:
- name: Run migrations
run: go build -o main && chmod +x ./main && PORT="4000" PG_USER="postgres" PG_PASSWORD="postgres" PG_PORT="5432" PG_HOST="0.0.0.0" PG_DATABASE="gists" GOOGLE_KEY="" GOOGLE_SECRET="" GITHUB_KEY="" GITHUB_SECRET="" PUBLIC_URL="http://localhost:4000" APP_KEY="DUMP_APP_KEY_FOR_TEST" SMTP_HOST="" MAIL_SMTP="" MAIL_PASSWORD="" SMTP_PORT="" FRONTEND_URL="http://localhost:3000" ./main migrate
- name: Run tests
run: PORT="4000" PG_USER="postgres" PG_PASSWORD="postgres" PG_PORT="5432" PG_HOST="0.0.0.0" PG_DATABASE="gists" GOOGLE_KEY="" GOOGLE_SECRET="" GITHUB_KEY="" GITHUB_SECRET="" PUBLIC_URL="http://localhost:4000" APP_KEY="DUMP_APP_KEY_FOR_TEST" SMTP_HOST="" MAIL_SMTP="" MAIL_PASSWORD="" SMTP_PORT="" FRONTEND_URL="http://localhost:3000" go test ./tests/
run: cd test && PORT="4000" PG_USER="postgres" PG_PASSWORD="postgres" PG_PORT="5432" PG_HOST="0.0.0.0" PG_DATABASE="gists" GOOGLE_KEY="" GOOGLE_SECRET="" GITHUB_KEY="" GITHUB_SECRET="" PUBLIC_URL="http://localhost:4000" APP_KEY="DUMP_APP_KEY_FOR_TEST" SMTP_HOST="" MAIL_SMTP="" MAIL_PASSWORD="" SMTP_PORT="" FRONTEND_URL="http://localhost:3000" go test
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ node_modules
tmp
.env
api
test/coverage
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,9 @@ just migrate
# or
migrate -path=migrations -database "postgresql://postgres:[email protected]:5432/gists?sslmode=disable" -verbose up
```

To rollback a migration :

```bash
migrate -path=migrations -database "postgresql://postgres:postgres@localhost:5432/gists?sslmode=disable" -verbose down 1
```
125 changes: 109 additions & 16 deletions gists/controller.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
package gists

import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
)

type GistControllerImpl struct{}
type GistControllerImpl struct {
gist_guard GistGuard
}

type GistSaveValidator struct {
Name string `json:"name"`
Content string `json:"content"`
Name string `json:"name" validate:"required"`
Content string `json:"content" validate:"required"`
OrgID string `json:"org_id,omitempty"`
Language string `json:"language,omitempty"`
Description string `json:"description,omitempty"`
Visibility string `json:"visibility,omitempty"`
}

func (g *GistControllerImpl) Save() fiber.Handler {
Expand All @@ -23,7 +27,21 @@ func (g *GistControllerImpl) Save() fiber.Handler {
if err := c.BodyParser(g); err != nil {
return c.Status(400).SendString("Request must be valid JSON with fields name and content as text")
}
gist, err := GistService.Save(g.Name, g.Content, owner_id, g.OrgID, g.Language, g.Description)
validate := validator.New(validator.WithRequiredStructEnabled())

err := validate.Struct(g)

if err != nil {
log.Error(err)
return c.Status(400).SendString("Request must be valid JSON with fields name and content as text")
}

visibility := "public"
if g.Visibility == "private" {
visibility = "private"
}

gist, err := GistService.Save(g.Name, g.Content, owner_id, g.OrgID, g.Language, g.Description, visibility)
if err != nil {
return c.Status(500).SendString(err.Error())
}
Expand All @@ -34,13 +52,24 @@ func (g *GistControllerImpl) Save() fiber.Handler {

func (g *GistControllerImpl) UpdateName() fiber.Handler {
return func(c *fiber.Ctx) error {
g := new(GistSaveValidator)
if err := c.BodyParser(g); err != nil {
g_body := new(GistSaveValidator)
if err := c.BodyParser(g_body); err != nil {
return c.Status(400).SendString("Request must be valid JSON with fields name and content as text")
}

owner_id := c.Locals("pub").(string)
gist, err := GistService.UpdateName(c.Params("id"), g.Name, owner_id)

can_edit, err := g.gist_guard.CanEdit(c.Params("id"), owner_id)

if err != nil {
return c.Status(500).SendString(err.Error())
}

if !can_edit {
return c.Status(403).SendString("You do not have permission to edit this gist")
}

gist, err := GistService.UpdateName(c.Params("id"), g_body.Name, owner_id)
if err != nil {
if err == ErrGistNotFound {
return c.Status(404).SendString(err.Error())
Expand All @@ -67,6 +96,16 @@ func (g *GistControllerImpl) FindByID() fiber.Handler {
return func(c *fiber.Ctx) error {
owner_id := c.Locals("pub").(string)

can_read, err := g.gist_guard.CanRead(c.Params("id"), owner_id)

if err != nil {
return c.Status(500).SendString(err.Error())
}

if !can_read {
return c.Status(403).SendString("You do not have permission to read this gist")
}

gist, err := GistService.FindByID(c.Params("id"), owner_id)
if err != nil {
return c.Status(404).SendString(err.Error())
Expand All @@ -85,6 +124,16 @@ func (g *GistControllerImpl) RawFindByID() fiber.Handler {
return func(c *fiber.Ctx) error {
owner_id := c.Locals("pub").(string)

can_read, err := g.gist_guard.CanRead(c.Params("id"), owner_id)

if err != nil {
return c.Status(500).SendString(err.Error())
}

if !can_read {
return c.Status(403).SendString("You do not have permission to read this gist")
}

gist, err := GistService.FindByID(c.Params("id"), owner_id)
if err != nil {
return c.Status(404).SendString(err.Error())
Expand All @@ -96,13 +145,24 @@ func (g *GistControllerImpl) RawFindByID() fiber.Handler {

func (g *GistControllerImpl) UpdateContent() fiber.Handler {
return func(c *fiber.Ctx) error {
g := new(GistSaveValidator)
if err := c.BodyParser(g); err != nil {
g_body := new(GistSaveValidator)
if err := c.BodyParser(g_body); err != nil {
return c.Status(400).SendString("Request must be valid JSON with fields name and content as text")
}

owner_id := c.Locals("pub").(string)
gist, err := GistService.UpdateContent(c.Params("id"), g.Content, owner_id)

can_edit, err := g.gist_guard.CanEdit(c.Params("id"), owner_id)

if err != nil {
return c.Status(500).SendString(err.Error())
}

if !can_edit {
return c.Status(403).SendString("You do not have permission to edit this gist")
}

gist, err := GistService.UpdateContent(c.Params("id"), g_body.Content, owner_id)
if err != nil {
if err == ErrGistNotFound {
return c.Status(404).SendString(err.Error())
Expand All @@ -115,12 +175,23 @@ func (g *GistControllerImpl) UpdateContent() fiber.Handler {

func (g *GistControllerImpl) UpdateLanguage() fiber.Handler {
return func(c *fiber.Ctx) error {
g := new(GistSaveValidator)
if err := c.BodyParser(g); err != nil {
g_body := new(GistSaveValidator)
if err := c.BodyParser(g_body); err != nil {
return c.Status(400).SendString("Request must be valid JSON with fields name and content as text")
}
owner_id := c.Locals("pub").(string)
gist, err := GistService.UpdateLanguage(c.Params("id"), g.Language, owner_id)

can_edit, err := g.gist_guard.CanEdit(c.Params("id"), owner_id)

if err != nil {
return c.Status(500).SendString(err.Error())
}

if !can_edit {
return c.Status(403).SendString("You do not have permission to edit this gist")
}

gist, err := GistService.UpdateLanguage(c.Params("id"), g_body.Language, owner_id)
if err != nil {
if err == ErrGistNotFound {
return c.Status(404).SendString(err.Error())
Expand All @@ -133,12 +204,23 @@ func (g *GistControllerImpl) UpdateLanguage() fiber.Handler {

func (g *GistControllerImpl) UpdateDescription() fiber.Handler {
return func(c *fiber.Ctx) error {
g := new(GistSaveValidator)
if err := c.BodyParser(g); err != nil {
g_body := new(GistSaveValidator)
if err := c.BodyParser(g_body); err != nil {
return c.Status(400).SendString("Request must be valid JSON with fields name and content as text")
}
owner_id := c.Locals("pub").(string)
gist, err := GistService.UpdateDescription(c.Params("id"), g.Description, owner_id)

can_edit, err := g.gist_guard.CanEdit(c.Params("id"), owner_id)

if err != nil {
return c.Status(500).SendString(err.Error())
}

if !can_edit {
return c.Status(403).SendString("You do not have permission to edit this gist")
}

gist, err := GistService.UpdateDescription(c.Params("id"), g_body.Description, owner_id)
if err != nil {
if err == ErrGistNotFound {
return c.Status(404).SendString(err.Error())
Expand All @@ -152,6 +234,17 @@ func (g *GistControllerImpl) UpdateDescription() fiber.Handler {
func (g *GistControllerImpl) Delete() fiber.Handler {
return func(c *fiber.Ctx) error {
owner_id := c.Locals("pub").(string)

can_edit, err := g.gist_guard.CanEdit(c.Params("id"), owner_id)

if err != nil {
return c.Status(500).SendString(err.Error())
}

if !can_edit {
return c.Status(403).SendString("You do not have permission to edit this gist")
}

if err := GistService.Delete(c.Params("id"), owner_id); err != nil {
if err == ErrGistNotFound {
return c.Status(404).SendString(err.Error())
Expand Down
70 changes: 70 additions & 0 deletions gists/gist_rights_model.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package gists

import (
"github.com/gistapp/api/storage"
"github.com/gistsapp/pogo/pogo"
"github.com/gofiber/fiber/v2/log"
)

type GistRights struct {
UserID string `json:"user_id" pogo:"user_id"`
GistID string `json:"gist_id" pogo:"gist_id"`
Right string `json:"right" pogo:"rights"`
}

type GistRightsModel interface {
Save() (*GistRights, error)
Delete() error
Update() (*GistRights, error)
GetByGistID() ([]GistRights, error)
GetByUserID() ([]GistRights, error)
GetByGistIDAndUserID() (*GistRights, error)
}

func (gr *GistRights) Save() (*GistRights, error) {
db := storage.PogoDatabase
gist := make([]GistRights, 0)
err := pogo.SuperQuery(db, "INSERT INTO user_gists(user_id, gist_id, rights) VALUES ($1, $2, $3) RETURNING :fields", &gist, gr.UserID, gr.GistID, gr.Right)

return &gist[0], err
}

func (gr *GistRights) Delete() error {
db := storage.PogoDatabase
_, err := db.Exec("DELETE FROM user_gists WHERE user_id = $1 AND gist_id = $2", gr.UserID, gr.GistID)
return err
}

func (gr *GistRights) Update() (*GistRights, error) {
db := storage.PogoDatabase
gist := make([]GistRights, 0)
err := pogo.SuperQuery(db, "UPDATE user_gists SET rights = $1 WHERE user_id = $2 AND gist_id = $3 RETURNING :fields", &gist, gr.Right, gr.UserID, gr.GistID)
return &gist[0], err
}

func (gr *GistRights) GetByGistID() ([]GistRights, error) {
db := storage.PogoDatabase
gists := make([]GistRights, 0)
err := pogo.SuperQuery(db, "SELECT :fields FROM user_gists WHERE gist_id = $1", &gists, gr.GistID)
return gists, err
}

func (gr *GistRights) GetByUserID() ([]GistRights, error) {
db := storage.PogoDatabase
gists := make([]GistRights, 0)
err := pogo.SuperQuery(db, "SELECT :fields FROM user_gists WHERE user_id = $1", &gists, gr.UserID)
return gists, err
}

func (gr *GistRights) GetByGistIDAndUserID() (*GistRights, error) {
log.Info(gr.UserID)
log.Info(gr.GistID)
db := storage.PogoDatabase
gists := make([]GistRights, 0)
err := pogo.SuperQuery(db, "SELECT :fields FROM user_gists WHERE user_id = $1 AND gist_id = $2", &gists, gr.UserID, gr.GistID)
log.Info(gists)
if len(gists) <= 0 {
return nil, nil
}
return &gists[0], err
}
Loading
Loading