package httpapi import ( "log/slog" "net/http" "time" "github.com/google/uuid" "github.com/syncova/syncova/packages/audit" "github.com/syncova/syncova/packages/auth" "github.com/syncova/syncova/packages/platform/logging" ) // roleHandler bedient Rollen und Berechtigungen (SYNCOVA_API.md §5). type roleHandler struct { // repository ist die Datenzugriffsschicht der Identitätsverwaltung. repository *auth.Repository // auditRecorder protokolliert Änderungen an Rollen. auditRecorder audit.Recorder // logger protokolliert technische Fehler. logger *slog.Logger } // roleRequest ist der Rumpf von POST und PATCH auf /roles. type roleRequest struct { // Name ist der technische Name der Rolle (nur beim Anlegen ausgewertet). Name string `json:"name"` // Description erklärt den Zweck der Rolle. Description *string `json:"description"` // Permissions sind die zuzuordnenden Berechtigungen. Permissions []string `json:"permissions"` } // handleListRoles bedient GET /roles. func (handler *roleHandler) handleListRoles(responseWriter http.ResponseWriter, request *http.Request) { requestLogger := logging.WithContext(request.Context(), handler.logger) loadedRoles, listError := handler.repository.ListRoles(request.Context()) if listError != nil { WriteError(responseWriter, request, requestLogger, NewInternalError(listError)) return } WriteSuccess(responseWriter, request, http.StatusOK, loadedRoles) } // handleGetRole bedient GET /roles/{id}. func (handler *roleHandler) handleGetRole(responseWriter http.ResponseWriter, request *http.Request) { requestLogger := logging.WithContext(request.Context(), handler.logger) roleID, parseError := parsePathUUID(request, "id") if parseError != nil { WriteError(responseWriter, request, requestLogger, parseError) return } loadedRole, loadError := handler.repository.FindRoleByID(request.Context(), roleID) if loadError != nil { WriteError(responseWriter, request, requestLogger, translateAuthError(loadError)) return } WriteSuccess(responseWriter, request, http.StatusOK, loadedRole) } // handleCreateRole bedient POST /roles. func (handler *roleHandler) handleCreateRole(responseWriter http.ResponseWriter, request *http.Request) { requestLogger := logging.WithContext(request.Context(), handler.logger) actingUser, _ := AuthenticatedUserFromContext(request.Context()) var rolePayload roleRequest if decodeError := decodeJSONBody(request, &rolePayload); decodeError != nil { WriteError(responseWriter, request, requestLogger, decodeError) return } if rolePayload.Name == "" { WriteError(responseWriter, request, requestLogger, NewValidationError("Der Rollenname ist erforderlich.")) return } // Eine Rolle ohne Berechtigungen wäre wirkungslos und fast immer ein Versehen. if len(rolePayload.Permissions) == 0 { WriteError(responseWriter, request, requestLogger, NewValidationError("Es muss mindestens eine Berechtigung zugeordnet werden.")) return } description := "" if rolePayload.Description != nil { description = *rolePayload.Description } createdRoleID, createError := handler.repository.CreateRole( request.Context(), rolePayload.Name, description, rolePayload.Permissions) if createError != nil { WriteError(responseWriter, request, requestLogger, translateAuthError(createError)) return } handler.recordRoleAudit(request, audit.ActionRoleCreated, createdRoleID, actingUser, map[string]any{ "name": rolePayload.Name, "berechtigungen": rolePayload.Permissions, }) createdRole, loadError := handler.repository.FindRoleByID(request.Context(), createdRoleID) if loadError != nil { WriteError(responseWriter, request, requestLogger, NewInternalError(loadError)) return } WriteSuccess(responseWriter, request, http.StatusCreated, createdRole) } // handleUpdateRole bedient PATCH /roles/{id}. func (handler *roleHandler) handleUpdateRole(responseWriter http.ResponseWriter, request *http.Request) { requestLogger := logging.WithContext(request.Context(), handler.logger) actingUser, _ := AuthenticatedUserFromContext(request.Context()) roleID, parseError := parsePathUUID(request, "id") if parseError != nil { WriteError(responseWriter, request, requestLogger, parseError) return } var rolePayload roleRequest if decodeError := decodeJSONBody(request, &rolePayload); decodeError != nil { WriteError(responseWriter, request, requestLogger, decodeError) return } if updateError := handler.repository.UpdateRole( request.Context(), roleID, rolePayload.Description, rolePayload.Permissions); updateError != nil { WriteError(responseWriter, request, requestLogger, translateAuthError(updateError)) return } handler.recordRoleAudit(request, audit.ActionRoleUpdated, roleID, actingUser, map[string]any{ "berechtigungen": rolePayload.Permissions, }) updatedRole, loadError := handler.repository.FindRoleByID(request.Context(), roleID) if loadError != nil { WriteError(responseWriter, request, requestLogger, NewInternalError(loadError)) return } WriteSuccess(responseWriter, request, http.StatusOK, updatedRole) } // handleDeleteRole bedient DELETE /roles/{id}. func (handler *roleHandler) handleDeleteRole(responseWriter http.ResponseWriter, request *http.Request) { requestLogger := logging.WithContext(request.Context(), handler.logger) actingUser, _ := AuthenticatedUserFromContext(request.Context()) roleID, parseError := parsePathUUID(request, "id") if parseError != nil { WriteError(responseWriter, request, requestLogger, parseError) return } if deleteError := handler.repository.DeleteRole(request.Context(), roleID); deleteError != nil { WriteError(responseWriter, request, requestLogger, translateAuthError(deleteError)) return } handler.recordRoleAudit(request, audit.ActionRoleDeleted, roleID, actingUser, nil) WriteSuccess(responseWriter, request, http.StatusNoContent, nil) } // handleListPermissions bedient GET /permissions. func (handler *roleHandler) handleListPermissions(responseWriter http.ResponseWriter, request *http.Request) { requestLogger := logging.WithContext(request.Context(), handler.logger) loadedPermissions, listError := handler.repository.ListPermissions(request.Context()) if listError != nil { WriteError(responseWriter, request, requestLogger, NewInternalError(listError)) return } WriteSuccess(responseWriter, request, http.StatusOK, loadedPermissions) } // recordRoleAudit protokolliert eine Rollenänderung. func (handler *roleHandler) recordRoleAudit(request *http.Request, auditAction audit.Action, roleID uuid.UUID, actingUser auth.User, auditDetails map[string]any) { correlationID, _ := logging.CorrelationIDFromContext(request.Context()) if recordError := handler.auditRecorder.Record(request.Context(), audit.Event{ UserID: &actingUser.ID, ActorUsername: actingUser.Username, Action: auditAction, EntityType: "role", EntityID: &roleID, Result: audit.ResultSuccess, IPAddress: clientIPAddress(request), UserAgent: request.UserAgent(), CorrelationID: correlationID, Details: auditDetails, }); recordError != nil { logging.WithContext(request.Context(), handler.logger).Error( "rollenänderung konnte nicht protokolliert werden", slog.String("action", string(auditAction)), slog.String("error", recordError.Error())) } } // --------------------------------------------------------------------------- // Auditprotokoll // --------------------------------------------------------------------------- // auditHandler bedient die Auditabfrage (SYNCOVA_API.md §17). type auditHandler struct { // auditRecorder liest die protokollierten Ereignisse. auditRecorder audit.Recorder // logger protokolliert technische Fehler. logger *slog.Logger } // handleListAuditEvents bedient GET /audit-events. func (handler *auditHandler) handleListAuditEvents(responseWriter http.ResponseWriter, request *http.Request) { requestLogger := logging.WithContext(request.Context(), handler.logger) page, pageSize := parsePagination(request) queryParameters := request.URL.Query() queryFilter := audit.Filter{ Action: queryParameters.Get("action"), Result: queryParameters.Get("result"), Page: page, PageSize: pageSize, } if rawUserID := queryParameters.Get("user_id"); rawUserID != "" { parsedUserID, parseError := uuid.Parse(rawUserID) if parseError != nil { WriteError(responseWriter, request, requestLogger, NewBadRequestError("Der Parameter user_id ist keine gültige UUID.")) return } queryFilter.UserID = &parsedUserID } // Zeitangaben werden in ISO 8601 erwartet, passend zur Ausgabe der API. if rawFrom := queryParameters.Get("from"); rawFrom != "" { parsedFrom, parseError := time.Parse(time.RFC3339, rawFrom) if parseError != nil { WriteError(responseWriter, request, requestLogger, NewBadRequestError("Der Parameter from muss im Format RFC 3339 angegeben werden.")) return } queryFilter.From = &parsedFrom } if rawTo := queryParameters.Get("to"); rawTo != "" { parsedTo, parseError := time.Parse(time.RFC3339, rawTo) if parseError != nil { WriteError(responseWriter, request, requestLogger, NewBadRequestError("Der Parameter to muss im Format RFC 3339 angegeben werden.")) return } queryFilter.To = &parsedTo } auditEvents, totalCount, queryError := handler.auditRecorder.Query(request.Context(), queryFilter) if queryError != nil { WriteError(responseWriter, request, requestLogger, NewInternalError(queryError)) return } WritePaginatedSuccess(responseWriter, request, auditEvents, PaginationMeta{ Page: page, PageSize: pageSize, Total: totalCount, }) }