diff --git a/service/internal/api/api.go b/service/internal/api/api.go index da94b0b..57b296b 100644 --- a/service/internal/api/api.go +++ b/service/internal/api/api.go @@ -321,40 +321,50 @@ func (api *oliveTinAPI) GetActionBinding(ctx ctx.Context, req *connect.Request[a func (api *oliveTinAPI) GetDashboard(ctx ctx.Context, req *connect.Request[apiv1.GetDashboardRequest]) (*connect.Response[apiv1.GetDashboardResponse], error) { user := acl.UserFromContext(ctx, api.cfg) - if user.IsGuest() && api.cfg.AuthRequireGuestsToLogin { - return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("guests are not allowed to access the dashboard")) + if err := api.checkDashboardAccess(user); err != nil { + return nil, err } - dashboardRenderRequest := &DashboardRenderRequest{ + dashboardRenderRequest := api.createDashboardRenderRequest(user) + + if api.isDefaultDashboard(req.Msg.Title) { + return api.buildDefaultDashboardResponse(dashboardRenderRequest) + } + + return api.buildCustomDashboardResponse(dashboardRenderRequest, req.Msg.Title) +} + +func (api *oliveTinAPI) checkDashboardAccess(user *acl.AuthenticatedUser) error { + if user.IsGuest() && api.cfg.AuthRequireGuestsToLogin { + return connect.NewError(connect.CodePermissionDenied, fmt.Errorf("guests are not allowed to access the dashboard")) + } + return nil +} + +func (api *oliveTinAPI) createDashboardRenderRequest(user *acl.AuthenticatedUser) *DashboardRenderRequest { + return &DashboardRenderRequest{ AuthenticatedUser: user, cfg: api.cfg, ex: api.executor, } +} - if req.Msg.Title == "default" || req.Msg.Title == "" || req.Msg.Title == "Actions" { - db := buildDefaultDashboard(dashboardRenderRequest) - res := &apiv1.GetDashboardResponse{ - Dashboard: db, - } - return connect.NewResponse(res), nil - } +func (api *oliveTinAPI) isDefaultDashboard(title string) bool { + return title == "default" || title == "" || title == "Actions" +} +func (api *oliveTinAPI) buildDefaultDashboardResponse(rr *DashboardRenderRequest) (*connect.Response[apiv1.GetDashboardResponse], error) { + db := buildDefaultDashboard(rr) res := &apiv1.GetDashboardResponse{ - Dashboard: renderDashboard(dashboardRenderRequest, req.Msg.Title), + Dashboard: db, } + return connect.NewResponse(res), nil +} - /* - if len(res.Actions) == 0 { - log.WithFields(log.Fields{ - "username": user.Username, - "usergroupLine": user.UsergroupLine, - "provider": user.Provider, - "acls": user.Acls, - "availableActions": len(api.cfg.Actions), - }).Warn("Zero actions found for user") - } - */ - +func (api *oliveTinAPI) buildCustomDashboardResponse(rr *DashboardRenderRequest, title string) (*connect.Response[apiv1.GetDashboardResponse], error) { + res := &apiv1.GetDashboardResponse{ + Dashboard: renderDashboard(rr, title), + } return connect.NewResponse(res), nil } @@ -595,31 +605,31 @@ func (api *oliveTinAPI) Init(ctx ctx.Context, req *connect.Request[apiv1.InitReq func (api *oliveTinAPI) buildRootDashboards(user *acl.AuthenticatedUser, dashboards []*config.DashboardComponent) []string { var rootDashboards []string + dashboardRenderRequest := api.createDashboardRenderRequest(user) - dashboardRenderRequest := &DashboardRenderRequest{ - AuthenticatedUser: user, - cfg: api.cfg, - ex: api.executor, - } + api.addDefaultDashboardIfNeeded(&rootDashboards, dashboardRenderRequest) + api.addCustomDashboards(&rootDashboards, dashboards, dashboardRenderRequest) - defaultDashboard := buildDefaultDashboard(dashboardRenderRequest) + return rootDashboards +} +func (api *oliveTinAPI) addDefaultDashboardIfNeeded(rootDashboards *[]string, rr *DashboardRenderRequest) { + defaultDashboard := buildDefaultDashboard(rr) if defaultDashboard != nil && len(defaultDashboard.Contents) > 0 { log.Infof("defaultDashboard: %+v", defaultDashboard.Contents) - rootDashboards = append(rootDashboards, "Actions") + *rootDashboards = append(*rootDashboards, "Actions") } +} +func (api *oliveTinAPI) addCustomDashboards(rootDashboards *[]string, dashboards []*config.DashboardComponent, rr *DashboardRenderRequest) { for _, dashboard := range dashboards { // We have to build the dashboard response instead of just looping over config.dashboards, // because we need to check if the user has access to the dashboard - db := renderDashboard(dashboardRenderRequest, dashboard.Title) - + db := renderDashboard(rr, dashboard.Title) if db != nil { - rootDashboards = append(rootDashboards, dashboard.Title) + *rootDashboards = append(*rootDashboards, dashboard.Title) } } - - return rootDashboards } func buildPublicOAuth2ProvidersList(cfg *config.Config) []*apiv1.OAuth2Provider { @@ -720,7 +730,7 @@ func (api *oliveTinAPI) GetEntity(ctx ctx.Context, req *connect.Request[apiv1.Ge log.Infof("msg: %+v", req.Msg) - if instances == nil || len(instances) == 0 { + if len(instances) == 0 { return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("entity type %s not found", req.Msg.Type)) } diff --git a/service/internal/api/dashboards.go b/service/internal/api/dashboards.go index 1b6441f..28f5c12 100644 --- a/service/internal/api/dashboards.go +++ b/service/internal/api/dashboards.go @@ -14,32 +14,44 @@ func renderDashboard(rr *DashboardRenderRequest, dashboardTitle string) *apiv1.D return buildDefaultDashboard(rr) } + return findAndRenderDashboard(rr, dashboardTitle) +} + +func findAndRenderDashboard(rr *DashboardRenderRequest, dashboardTitle string) *apiv1.Dashboard { for _, dashboard := range rr.cfg.Dashboards { if dashboard.Title != dashboardTitle { continue } if len(dashboard.Contents) == 0 { - log.WithFields(log.Fields{ - "dashboard": dashboard.Title, - "username": rr.AuthenticatedUser.Username, - }).Debugf("Dashboard has no readable contents, so it will not be visible in the web ui") + logEmptyDashboard(dashboard.Title, rr.AuthenticatedUser.Username) return nil } - return &apiv1.Dashboard{ - Title: dashboard.Title, - Contents: sortActions(removeNulls(getDashboardComponentContents(dashboard, rr))), - } + return buildDashboardFromConfig(dashboard, rr) } return nil } +func logEmptyDashboard(dashboardTitle, username string) { + log.WithFields(log.Fields{ + "dashboard": dashboardTitle, + "username": username, + }).Debugf("Dashboard has no readable contents, so it will not be visible in the web ui") +} + +func buildDashboardFromConfig(dashboard *config.DashboardComponent, rr *DashboardRenderRequest) *apiv1.Dashboard { + return &apiv1.Dashboard{ + Title: dashboard.Title, + Contents: sortActions(removeNulls(getDashboardComponentContents(dashboard, rr))), + } +} + //gocyclo:ignore func buildDefaultDashboard(rr *DashboardRenderRequest) *apiv1.Dashboard { db := &apiv1.Dashboard{ - Title: "Default", + Title: "Actions", Contents: make([]*apiv1.DashboardComponent, 0), } @@ -112,28 +124,40 @@ func removeNulls(components []*apiv1.DashboardComponent) []*apiv1.DashboardCompo func getDashboardComponentContents(dashboard *config.DashboardComponent, rr *DashboardRenderRequest) []*apiv1.DashboardComponent { ret := make([]*apiv1.DashboardComponent, 0) + rootFieldset := createRootFieldset() - rootFieldset := &apiv1.DashboardComponent{ + for _, subitem := range dashboard.Contents { + processDashboardSubitem(subitem, rr, &ret, rootFieldset) + } + + return appendRootFieldsetIfNeeded(ret, rootFieldset) +} + +func createRootFieldset() *apiv1.DashboardComponent { + return &apiv1.DashboardComponent{ Type: "fieldset", Title: "Actions", Contents: make([]*apiv1.DashboardComponent, 0), } +} - for _, subitem := range dashboard.Contents { - if subitem.Type == "fieldset" && subitem.Entity != "" { - ret = append(ret, buildEntityFieldsets(subitem.Entity, subitem, rr)...) - } else if subitem.Type == "fieldset" { - // Handle regular fieldsets by creating them directly - ret = append(ret, buildDashboardComponentSimple(subitem, rr)) - } else { - rootFieldset.Contents = append(rootFieldset.Contents, buildDashboardComponentSimple(subitem, rr)) - } +func processDashboardSubitem(subitem *config.DashboardComponent, rr *DashboardRenderRequest, ret *[]*apiv1.DashboardComponent, rootFieldset *apiv1.DashboardComponent) { + if subitem.Type != "fieldset" { + rootFieldset.Contents = append(rootFieldset.Contents, buildDashboardComponentSimple(subitem, rr)) + return } + if subitem.Entity != "" { + *ret = append(*ret, buildEntityFieldsets(subitem.Entity, subitem, rr)...) + } else { + *ret = append(*ret, buildDashboardComponentSimple(subitem, rr)) + } +} + +func appendRootFieldsetIfNeeded(ret []*apiv1.DashboardComponent, rootFieldset *apiv1.DashboardComponent) []*apiv1.DashboardComponent { if len(rootFieldset.Contents) > 0 { ret = append(ret, rootFieldset) } - return ret } diff --git a/service/internal/executor/arguments_test.go b/service/internal/executor/arguments_test.go index b541e00..c64258e 100644 --- a/service/internal/executor/arguments_test.go +++ b/service/internal/executor/arguments_test.go @@ -7,8 +7,9 @@ import ( config "github.com/OliveTin/OliveTin/internal/config" "github.com/OliveTin/OliveTin/internal/entities" - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestSanitizeUnsafe(t *testing.T) { @@ -254,9 +255,9 @@ func TestTypeSafetyCheckRawStringMultiline(t *testing.T) { func TestTypeSafetyCheckUnicodeIdentifier(t *testing.T) { tests := []struct { - name string - field string - value string + name string + field string + value string expectsError bool }{ {"Valid unicode identifier", "name", "hello_world", false}, @@ -273,24 +274,35 @@ func TestTypeSafetyCheckUnicodeIdentifier(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := TypeSafetyCheck(tt.field, tt.value, "unicode_identifier") - - if tt.expectsError { - if err == nil { - t.Errorf("Expected error for value '%s', but got none", tt.value) - } else { - t.Logf("Received expected error for value '%s': %v", tt.value, err) - } - } else { - if err != nil { - t.Errorf("Expected no error for value '%s', but got: %v", tt.value, err) - } else { - t.Logf("No error for valid value '%s' as expected", tt.value) - } - } + validateTypeSafetyResult(t, tt.value, tt.expectsError, err) }) } } +func validateTypeSafetyResult(t *testing.T, value string, expectsError bool, err error) { + if expectsError { + assertErrorExpected(t, value, err) + } else { + assertNoErrorExpected(t, value, err) + } +} + +func assertErrorExpected(t *testing.T, value string, err error) { + if err == nil { + t.Errorf("Expected error for value '%s', but got none", value) + } else { + t.Logf("Received expected error for value '%s': %v", value, err) + } +} + +func assertNoErrorExpected(t *testing.T, value string, err error) { + if err != nil { + t.Errorf("Expected no error for value '%s', but got: %v", value, err) + } else { + t.Logf("No error for valid value '%s' as expected", value) + } +} + func TestTypeSafetyCheckAsciiIdentifier(t *testing.T) { tests := []struct { name string