diff --git a/connect/cmd/connect-app/main.go b/connect/cmd/connect-app/main.go index 52a6d898..5c973102 100644 --- a/connect/cmd/connect-app/main.go +++ b/connect/cmd/connect-app/main.go @@ -18,9 +18,12 @@ import ( "gitea.perlbach24.de/scripte/nexarch/common/bearerauth" "gitea.perlbach24.de/scripte/nexarch/common/moduleauth" "gitea.perlbach24.de/scripte/nexarch/common/policyclient" + "gitea.perlbach24.de/scripte/nexarch/connect/internal/archiveclient" "gitea.perlbach24.de/scripte/nexarch/connect/internal/connectorsetupapi" "gitea.perlbach24.de/scripte/nexarch/connect/internal/credential" "gitea.perlbach24.de/scripte/nexarch/connect/internal/crypto" + "gitea.perlbach24.de/scripte/nexarch/connect/internal/datevapi" + "gitea.perlbach24.de/scripte/nexarch/connect/internal/datevexport" "gitea.perlbach24.de/scripte/nexarch/connect/internal/dms2import" "gitea.perlbach24.de/scripte/nexarch/connect/internal/dms2importapi" "gitea.perlbach24.de/scripte/nexarch/connect/internal/fieldmapping" @@ -56,6 +59,25 @@ func buildPolicyClient() *policyclient.Client { return policyclient.New(policyURL, token) } +// buildDatevExports liefert nil ohne NEXARCH_CONNECT_APP_ARCHIVE_URL; dann ist +// /datev/exports nicht registriert. Die DATEV-Uebertragung ist hier nicht verdrahtet. +func buildDatevExports(pool *pgxpool.Pool) *datevexport.Store { + archiveURL := os.Getenv("NEXARCH_CONNECT_APP_ARCHIVE_URL") + if archiveURL == "" { + log.Print("connect-app: NEXARCH_CONNECT_APP_ARCHIVE_URL fehlt, /datev/exports nicht registriert") + return nil + } + token := os.Getenv("NEXARCH_CONNECT_APP_ARCHIVE_SERVICE_TOKEN") + if token == "" { + log.Fatal("connect-app: NEXARCH_CONNECT_APP_ARCHIVE_SERVICE_TOKEN ist erforderlich, wenn NEXARCH_CONNECT_APP_ARCHIVE_URL gesetzt ist") + } + return &datevexport.Store{ + Pool: pool, + Archive: archiveclient.New(archiveURL, token), + CallbackURL: os.Getenv("NEXARCH_CONNECT_APP_DATEV_CALLBACK_URL"), + } +} + func main() { if !killswitch.Enabled() { log.Printf("connect-app: deaktiviert (%s=false), kein Start", killswitch.EnvEnabled) @@ -98,6 +120,9 @@ func main() { statusapi.NewServer(syncjob.NewStore(pool)).WithPolicy(policy).RegisterRoutes(mux) dms2importapi.NewServer(mappings, dms2import.NewStore(pool)).RegisterRoutes(mux) connectorsetupapi.NewServer(credentials, mappings).WithPolicy(policy).RegisterRoutes(mux) + if exports := buildDatevExports(pool); exports != nil { + datevapi.NewServer(exports, nil).WithPolicy(policy).RegisterRoutes(mux) + } srv := &http.Server{ Addr: addr, diff --git a/connect/internal/datevapi/datevapi.go b/connect/internal/datevapi/datevapi.go index 5a3c9128..e27a0e3e 100644 --- a/connect/internal/datevapi/datevapi.go +++ b/connect/internal/datevapi/datevapi.go @@ -9,6 +9,7 @@ import ( "net/http" "time" + "gitea.perlbach24.de/scripte/nexarch/common/policyclient" "gitea.perlbach24.de/scripte/nexarch/connect/internal/datevexport" "gitea.perlbach24.de/scripte/nexarch/connect/internal/datevtransfer" ) @@ -16,6 +17,12 @@ import ( type Server struct { Exports *datevexport.Store Transfers *datevtransfer.Store + policy *policyclient.Client +} + +func (s *Server) WithPolicy(client *policyclient.Client) *Server { + s.policy = client + return s } func NewServer(exports *datevexport.Store, transfers *datevtransfer.Store) *Server { @@ -23,8 +30,32 @@ func NewServer(exports *datevexport.Store, transfers *datevtransfer.Store) *Serv } func (s *Server) RegisterRoutes(mux *http.ServeMux) { - mux.HandleFunc("POST /datev/exports", s.requireIdentity(s.handleTrigger)) - mux.HandleFunc("GET /datev/exports", s.requireIdentity(s.handleList)) + mux.HandleFunc("POST /datev/exports", s.requireIdentity(s.requirePermission("connect.datev.write", s.handleTrigger))) + mux.HandleFunc("GET /datev/exports", s.requireIdentity(s.requirePermission("connect.datev.read", s.handleList))) +} + +func (s *Server) requirePermission(permission string, next func(w http.ResponseWriter, r *http.Request, tenantSlug string)) func(w http.ResponseWriter, r *http.Request, tenantSlug string) { + return func(w http.ResponseWriter, r *http.Request, tenantSlug string) { + if s.policy != nil { + allowed, err := s.policy.Allowed(r.Context(), tenantSlug, roleOf(r), permission) + if err != nil { + writeError(w, http.StatusServiceUnavailable, "berechtigungsdienst nicht erreichbar") + return + } + if !allowed { + writeError(w, http.StatusForbidden, "keine berechtigung") + return + } + } + next(w, r, tenantSlug) + } +} + +func roleOf(r *http.Request) string { + if role := r.Header.Get("X-User-Role"); role != "" { + return role + } + return "user" } func (s *Server) requireIdentity(next func(w http.ResponseWriter, r *http.Request, tenantSlug string)) http.HandlerFunc { diff --git a/connect/internal/datevapi/datevapi_policy_test.go b/connect/internal/datevapi/datevapi_policy_test.go new file mode 100644 index 00000000..def1b16e --- /dev/null +++ b/connect/internal/datevapi/datevapi_policy_test.go @@ -0,0 +1,46 @@ +package datevapi + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "gitea.perlbach24.de/scripte/nexarch/common/policyclient" +) + +func TestPolicy_PermissionAndRoleReachCore(t *testing.T) { + type ask struct{ role, permission string } + var got []ask + policyAPI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var body struct { + Role string `json:"role"` + Permission string `json:"permission"` + } + _ = json.NewDecoder(r.Body).Decode(&body) + got = append(got, ask{body.Role, body.Permission}) + _ = json.NewEncoder(w).Encode(map[string]bool{"allowed": false}) + })) + defer policyAPI.Close() + mux := http.NewServeMux() + (&Server{}).WithPolicy(policyclient.New(policyAPI.URL, "tok")).RegisterRoutes(mux) + + for _, c := range []struct{ method, role string }{{"GET", "tenant_admin"}, {"POST", ""}} { + req := httptest.NewRequest(c.method, "/datev/exports", strings.NewReader("{}")) + req.Header.Set("X-User-Id", "u1") + req.Header.Set("X-Tenant-Slug", "acme") + if c.role != "" { + req.Header.Set("X-User-Role", c.role) + } + rec := httptest.NewRecorder() + mux.ServeHTTP(rec, req) + if rec.Code != http.StatusForbidden { + t.Fatalf("%s: erwartet 403, bekam %d", c.method, rec.Code) + } + } + want := []ask{{"tenant_admin", "connect.datev.read"}, {"user", "connect.datev.write"}} + if len(got) != 2 || got[0] != want[0] || got[1] != want[1] { + t.Fatalf("erwartet %v, bekam %v", want, got) + } +}