Podsetnici: sprečen IDOR — provera vlasništva pre izmene/završavanja/brisanja
This commit is contained in:
@@ -50,7 +50,8 @@ Legenda ozbiljnosti: 🔴 visok · 🟡 srednji · 🟢 nizak
|
|||||||
|
|
||||||
## 4. Handler sloj — validacija, CSRF, autorizacija
|
## 4. Handler sloj — validacija, CSRF, autorizacija
|
||||||
|
|
||||||
- [ ] 🔴 **IDOR / broken access control na podsetnicima (ličnim podsetnicima).**
|
- [x] 🔴 **IDOR / broken access control na podsetnicima (ličnim podsetnicima). ISPRAVLJENO.**
|
||||||
|
Dodata `korisnikSmeDaMenjaPodsetnik(k, p)` provera (`internal/handler/podsetnici.go`) — dozvoljava izmenu/završavanje/brisanje ako je korisnik admin/superadmin (`middleware.JeAdmin`) ili je `p.KorisnikID` tačno njegov ID; u suprotnom `403 Forbidden`. Primenjeno u sve tri funkcije: `SacuvajIzmenePodsetnika` (sad prvo učitava postojeći podsetnik pre izmene), `OznaciPodsetnik` i `ObrisiPodsetnik` (sad prvo učitava podsetnik pre brisanja, ranije je brisao direktno po ID-u bez čitanja). Dodat `internal/handler/podsetnici_test.go` sa 6 test-slučajeva (radnik/tuđi radnik/admin × svoj/tuđi/nedodeljen podsetnik) — svi prolaze.
|
||||||
`internal/handler/podsetnici.go`:
|
`internal/handler/podsetnici.go`:
|
||||||
- `SacuvajIzmenePodsetnika` (red 166-195) — učitava `id` iz URL-a, poziva `PodsetnikRepo.Izmeni` BEZ provere da `podsetnik.KorisnikID` pripada ulogovanom korisniku (ili je izmenilac admin/superadmin).
|
- `SacuvajIzmenePodsetnika` (red 166-195) — učitava `id` iz URL-a, poziva `PodsetnikRepo.Izmeni` BEZ provere da `podsetnik.KorisnikID` pripada ulogovanom korisniku (ili je izmenilac admin/superadmin).
|
||||||
- `OznaciPodsetnik` (red 198-218) — isto, menja status završenosti bilo kog podsetnika po ID-u bez provere vlasništva.
|
- `OznaciPodsetnik` (red 198-218) — isto, menja status završenosti bilo kog podsetnika po ID-u bez provere vlasništva.
|
||||||
|
|||||||
@@ -172,6 +172,16 @@ func (h *Handler) SacuvajIzmenePodsetnika(w http.ResponseWriter, r *http.Request
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
postojeci, err := h.PodsetnikRepo.DohvatiID(r.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Podsetnik nije pronađen", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !korisnikSmeDaMenjaPodsetnik(k, postojeci) {
|
||||||
|
http.Error(w, "Nemate dozvolu da menjate ovaj podsetnik", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
|
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
@@ -196,6 +206,8 @@ func (h *Handler) SacuvajIzmenePodsetnika(w http.ResponseWriter, r *http.Request
|
|||||||
|
|
||||||
// OznaciPodsetnik prima POST zahtev i menja status završenosti podsetnika
|
// OznaciPodsetnik prima POST zahtev i menja status završenosti podsetnika
|
||||||
func (h *Handler) OznaciPodsetnik(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) OznaciPodsetnik(w http.ResponseWriter, r *http.Request) {
|
||||||
|
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||||
|
|
||||||
id, err := parseID(chi.URLParam(r, "id"))
|
id, err := parseID(chi.URLParam(r, "id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Neispravan ID podsetnika", http.StatusBadRequest)
|
http.Error(w, "Neispravan ID podsetnika", http.StatusBadRequest)
|
||||||
@@ -208,6 +220,10 @@ func (h *Handler) OznaciPodsetnik(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "Podsetnik nije pronađen", http.StatusNotFound)
|
http.Error(w, "Podsetnik nije pronađen", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if !korisnikSmeDaMenjaPodsetnik(k, podsetnik) {
|
||||||
|
http.Error(w, "Nemate dozvolu da menjate ovaj podsetnik", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := h.PodsetnikRepo.OznaciZavrsenim(r.Context(), id, !podsetnik.Zavrseno); err != nil {
|
if err := h.PodsetnikRepo.OznaciZavrsenim(r.Context(), id, !podsetnik.Zavrseno); err != nil {
|
||||||
http.Error(w, "Greška pri ažuriranju statusa", http.StatusInternalServerError)
|
http.Error(w, "Greška pri ažuriranju statusa", http.StatusInternalServerError)
|
||||||
@@ -219,12 +235,24 @@ func (h *Handler) OznaciPodsetnik(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// ObrisiPodsetnik prima POST zahtev i briše podsetnik po ID-u
|
// ObrisiPodsetnik prima POST zahtev i briše podsetnik po ID-u
|
||||||
func (h *Handler) ObrisiPodsetnik(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) ObrisiPodsetnik(w http.ResponseWriter, r *http.Request) {
|
||||||
|
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||||
|
|
||||||
id, err := parseID(chi.URLParam(r, "id"))
|
id, err := parseID(chi.URLParam(r, "id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Neispravan ID podsetnika", http.StatusBadRequest)
|
http.Error(w, "Neispravan ID podsetnika", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
podsetnik, err := h.PodsetnikRepo.DohvatiID(r.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Podsetnik nije pronađen", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !korisnikSmeDaMenjaPodsetnik(k, podsetnik) {
|
||||||
|
http.Error(w, "Nemate dozvolu da brišete ovaj podsetnik", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := h.PodsetnikRepo.Obrisi(r.Context(), id); err != nil {
|
if err := h.PodsetnikRepo.Obrisi(r.Context(), id); err != nil {
|
||||||
http.Error(w, "Greška pri brisanju podsetnika", http.StatusInternalServerError)
|
http.Error(w, "Greška pri brisanju podsetnika", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@@ -233,6 +261,17 @@ func (h *Handler) ObrisiPodsetnik(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Redirect(w, r, "/podsetnici?obrisan=1", http.StatusSeeOther)
|
http.Redirect(w, r, "/podsetnici?obrisan=1", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// korisnikSmeDaMenjaPodsetnik proverava vlasništvo nad podsetnikom — sprečava da jedan
|
||||||
|
// korisnik (npr. radnik) menja/završava/briše tuđi podsetnik pogađanjem ID-a u URL-u.
|
||||||
|
// Admin/superadmin smeju sve (isti kriterijum kao za dodelu podsetnika drugom korisniku
|
||||||
|
// u parseFormuPodsetnika); radnik sme samo podsetnik dodeljen njemu lično.
|
||||||
|
func korisnikSmeDaMenjaPodsetnik(k *model.Korisnik, p *model.Podsetnik) bool {
|
||||||
|
if middleware.JeAdmin(k) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return p.KorisnikID != nil && k != nil && *p.KorisnikID == k.ID
|
||||||
|
}
|
||||||
|
|
||||||
// parseFormuPodsetnika čita polja iz HTTP forme, validira ih i vraća model i eventualnu grešku
|
// parseFormuPodsetnika čita polja iz HTTP forme, validira ih i vraća model i eventualnu grešku
|
||||||
func parseFormuPodsetnika(r *http.Request, k *model.Korisnik) (model.Podsetnik, string) {
|
func parseFormuPodsetnika(r *http.Request, k *model.Korisnik) (model.Podsetnik, string) {
|
||||||
naslov := strings.TrimSpace(r.FormValue("naslov"))
|
naslov := strings.TrimSpace(r.FormValue("naslov"))
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"ntech/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestKorisnikSmeDaMenjaPodsetnik proverava zaštitu od IDOR-a: radnik sme da menja/briše
|
||||||
|
// samo sopstveni podsetnik, admin/superadmin smeju bilo koji.
|
||||||
|
func TestKorisnikSmeDaMenjaPodsetnik(t *testing.T) {
|
||||||
|
radnik := &model.Korisnik{ID: 1, Uloga: "radnik"}
|
||||||
|
drugiRadnik := &model.Korisnik{ID: 2, Uloga: "radnik"}
|
||||||
|
admin := &model.Korisnik{ID: 3, Uloga: "admin"}
|
||||||
|
|
||||||
|
svojPodsetnik := &model.Podsetnik{ID: 100, KorisnikID: p(1)}
|
||||||
|
tudjPodsetnik := &model.Podsetnik{ID: 101, KorisnikID: p(2)}
|
||||||
|
nedodeljenPodsetnik := &model.Podsetnik{ID: 102, KorisnikID: nil}
|
||||||
|
|
||||||
|
slucajevi := []struct {
|
||||||
|
naziv string
|
||||||
|
korisnik *model.Korisnik
|
||||||
|
podsetnik *model.Podsetnik
|
||||||
|
ocekivano bool
|
||||||
|
}{
|
||||||
|
{"radnik menja svoj podsetnik", radnik, svojPodsetnik, true},
|
||||||
|
{"radnik ne sme tuđi podsetnik", radnik, tudjPodsetnik, false},
|
||||||
|
{"drugi radnik ne sme tuđi podsetnik", drugiRadnik, svojPodsetnik, false},
|
||||||
|
{"radnik ne sme nedodeljen podsetnik", radnik, nedodeljenPodsetnik, false},
|
||||||
|
{"admin sme tuđi podsetnik", admin, tudjPodsetnik, true},
|
||||||
|
{"admin sme nedodeljen podsetnik", admin, nedodeljenPodsetnik, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, sc := range slucajevi {
|
||||||
|
t.Run(sc.naziv, func(t *testing.T) {
|
||||||
|
dobijeno := korisnikSmeDaMenjaPodsetnik(sc.korisnik, sc.podsetnik)
|
||||||
|
if dobijeno != sc.ocekivano {
|
||||||
|
t.Errorf("korisnikSmeDaMenjaPodsetnik(%+v, %+v) = %v, očekivano %v",
|
||||||
|
sc.korisnik, sc.podsetnik, dobijeno, sc.ocekivano)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user