diff --git a/go.mod b/go.mod index 429b843e4b7..e5ab2193201 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,7 @@ require ( github.com/onsi/ginkgo/v2 v2.7.0 github.com/onsi/gomega v1.24.1 github.com/orcaman/concurrent-map v1.0.0 - github.com/owncloud/libre-graph-api-go v1.0.1 + github.com/owncloud/libre-graph-api-go v1.0.2-0.20230105141655-9384face4d5d github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.14.0 github.com/rs/zerolog v1.28.0 diff --git a/go.sum b/go.sum index 4a009167dcb..ef8c4275ce3 100644 --- a/go.sum +++ b/go.sum @@ -1058,8 +1058,8 @@ github.com/oracle/oci-go-sdk v24.3.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35uk github.com/orcaman/concurrent-map v1.0.0 h1:I/2A2XPCb4IuQWcQhBhSwGfiuybl/J0ev9HDbW65HOY= github.com/orcaman/concurrent-map v1.0.0/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI= github.com/ovh/go-ovh v1.1.0/go.mod h1:AxitLZ5HBRPyUd+Zl60Ajaag+rNTdVXWIkzfrVuTXWA= -github.com/owncloud/libre-graph-api-go v1.0.1 h1:wj3aQQr/yDPoc97ddg7DCadvMx6ui6N7re/oRV9+yNs= -github.com/owncloud/libre-graph-api-go v1.0.1/go.mod h1:579sFrPP7aP24LZXGPopLfvE+hAka/2DYHk0+Ij+w+U= +github.com/owncloud/libre-graph-api-go v1.0.2-0.20230105141655-9384face4d5d h1:aqVf2yJEdSgFQd3k5fnwtYxjTwC/UREAKTZIzeupwHg= +github.com/owncloud/libre-graph-api-go v1.0.2-0.20230105141655-9384face4d5d/go.mod h1:iKdVH6nYpI8RBeK9sjeLfzrPByST6r9d+NG2IJHoJmU= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= diff --git a/ocis/pkg/init/init.go b/ocis/pkg/init/init.go index 3663172a0ec..560af63144d 100644 --- a/ocis/pkg/init/init.go +++ b/ocis/pkg/init/init.go @@ -50,11 +50,15 @@ type LdapBasedService struct { type Events struct { TLSInsecure bool `yaml:"tls_insecure"` } +type GraphApplication struct { + ID string `yaml:"id"` +} type GraphService struct { - Events Events - Spaces InsecureService - Identity LdapBasedService + Application GraphApplication + Events Events + Spaces InsecureService + Identity LdapBasedService } type ServiceUserPasswordsSettings struct { @@ -219,6 +223,7 @@ func CreateConfig(insecure, forceOverwrite bool, configPath, adminPassword strin systemUserID := uuid.Must(uuid.NewV4()).String() adminUserID := uuid.Must(uuid.NewV4()).String() + graphApplicationID := uuid.Must(uuid.NewV4()).String() storageUsersMountID := uuid.Must(uuid.NewV4()).String() idmServicePassword, err := generators.GenerateRandomPassword(passwordLength) @@ -306,6 +311,9 @@ func CreateConfig(insecure, forceOverwrite bool, configPath, adminPassword strin }, }, Graph: GraphService{ + Application: GraphApplication{ + ID: graphApplicationID, + }, Identity: LdapBasedService{ Ldap: LdapSettings{ BindPassword: idmServicePassword, diff --git a/services/graph/pkg/config/application.go b/services/graph/pkg/config/application.go new file mode 100644 index 00000000000..c33af28652c --- /dev/null +++ b/services/graph/pkg/config/application.go @@ -0,0 +1,7 @@ +package config + +// Application defines the available graph application configuration. +type Application struct { + ID string `yaml:"id" env:"GRAPH_APPLICATION_ID" desc:"The ocis application id shown in the graph. All app roles are tied to this."` + DisplayName string `yaml:"displayname" env:"GRAPH_APPLICATION_DISPLAYNAME" desc:"The oCIS application name"` +} diff --git a/services/graph/pkg/config/config.go b/services/graph/pkg/config/config.go index b3618c72e96..5b2f00e5189 100644 --- a/services/graph/pkg/config/config.go +++ b/services/graph/pkg/config/config.go @@ -25,9 +25,10 @@ type Config struct { TokenManager *TokenManager `yaml:"token_manager"` GRPCClientTLS *shared.GRPCClientTLS `yaml:"grpc_client_tls"` - Spaces Spaces `yaml:"spaces"` - Identity Identity `yaml:"identity"` - Events Events `yaml:"events"` + Application Application `yaml:"application"` + Spaces Spaces `yaml:"spaces"` + Identity Identity `yaml:"identity"` + Events Events `yaml:"events"` Context context.Context `yaml:"-"` } diff --git a/services/graph/pkg/config/defaults/defaultconfig.go b/services/graph/pkg/config/defaults/defaultconfig.go index 1d06dad3fdb..023efe04939 100644 --- a/services/graph/pkg/config/defaults/defaultconfig.go +++ b/services/graph/pkg/config/defaults/defaultconfig.go @@ -31,6 +31,9 @@ func DefaultConfig() *config.Config { Service: config.Service{ Name: "graph", }, + Application: config.Application{ + DisplayName: "ownCloud Infinite Scale", + }, API: config.API{ GroupMembersPatchLimit: 20, }, diff --git a/services/graph/pkg/config/parser/parse.go b/services/graph/pkg/config/parser/parse.go index d0fa4780b82..ffd91247ea8 100644 --- a/services/graph/pkg/config/parser/parse.go +++ b/services/graph/pkg/config/parser/parse.go @@ -2,8 +2,10 @@ package parser import ( "errors" + "fmt" ociscfg "github.com/owncloud/ocis/v2/ocis-pkg/config" + defaults2 "github.com/owncloud/ocis/v2/ocis-pkg/config/defaults" "github.com/owncloud/ocis/v2/ocis-pkg/shared" "github.com/owncloud/ocis/v2/services/graph/pkg/config" "github.com/owncloud/ocis/v2/services/graph/pkg/config/defaults" @@ -42,5 +44,13 @@ func Validate(cfg *config.Config) error { return shared.MissingLDAPBindPassword(cfg.Service.Name) } + if cfg.Application.ID == "" { + return fmt.Errorf("The application ID has not been configured for %s. "+ + "Make sure your %s config contains the proper values "+ + "(e.g. by running ocis init or setting it manually in "+ + "the config/corresponding environment variable).", + "graph", defaults2.BaseConfigPath()) + } + return nil } diff --git a/services/graph/pkg/service/v0/application.go b/services/graph/pkg/service/v0/application.go new file mode 100644 index 00000000000..c7c7c7a51c3 --- /dev/null +++ b/services/graph/pkg/service/v0/application.go @@ -0,0 +1,77 @@ +package svc + +import ( + "fmt" + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" + libregraph "github.com/owncloud/libre-graph-api-go" + settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0" + "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode" +) + +// ListApplications implements the Service interface. +func (g Graph) ListApplications(w http.ResponseWriter, r *http.Request) { + logger := g.logger.SubloggerWithRequestID(r.Context()) + logger.Info().Interface("query", r.URL.Query()).Msg("calling list applications") + + lbr, err := g.roleService.ListRoles(r.Context(), &settingssvc.ListBundlesRequest{}) + if err != nil { + logger.Error().Err(err).Msg("could not list roles: transport error") + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) + return + } + + roles := make([]libregraph.AppRole, 0, len(lbr.Bundles)) + for _, bundle := range lbr.GetBundles() { + role := libregraph.NewAppRole(bundle.GetId()) + role.SetDisplayName(bundle.GetDisplayName()) + roles = append(roles, *role) + } + + application := libregraph.NewApplication(g.config.Application.ID) + application.SetDisplayName(g.config.Application.DisplayName) + application.SetAppRoles(roles) + + applications := []*libregraph.Application{ + application, + } + + render.Status(r, http.StatusOK) + render.JSON(w, r, &ListResponse{Value: applications}) +} + +// GetApplication implements the Service interface. +func (g Graph) GetApplication(w http.ResponseWriter, r *http.Request) { + logger := g.logger.SubloggerWithRequestID(r.Context()) + logger.Info().Interface("query", r.URL.Query()).Msg("calling get application") + + applicationID := chi.URLParam(r, "applicationID") + + if applicationID != g.config.Application.ID { + errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, fmt.Sprintf("requested id %s does not match expected application id %v", applicationID, g.config.Application.ID)) + return + } + + lbr, err := g.roleService.ListRoles(r.Context(), &settingssvc.ListBundlesRequest{}) + if err != nil { + logger.Error().Err(err).Msg("could not list roles: transport error") + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) + return + } + + roles := make([]libregraph.AppRole, 0, len(lbr.Bundles)) + for _, bundle := range lbr.GetBundles() { + role := libregraph.NewAppRole(bundle.GetId()) + role.SetDisplayName(bundle.GetDisplayName()) + roles = append(roles, *role) + } + + application := libregraph.NewApplication(applicationID) + application.SetDisplayName(g.config.Application.DisplayName) + application.SetAppRoles(roles) + + render.Status(r, http.StatusOK) + render.JSON(w, r, application) +} diff --git a/services/graph/pkg/service/v0/application_test.go b/services/graph/pkg/service/v0/application_test.go new file mode 100644 index 00000000000..028ab59408c --- /dev/null +++ b/services/graph/pkg/service/v0/application_test.go @@ -0,0 +1,135 @@ +package svc_test + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + + "github.com/go-chi/chi/v5" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + libregraph "github.com/owncloud/libre-graph-api-go" + "github.com/stretchr/testify/mock" + + ogrpc "github.com/owncloud/ocis/v2/ocis-pkg/service/grpc" + "github.com/owncloud/ocis/v2/ocis-pkg/shared" + settingsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/settings/v0" + settings "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0" + "github.com/owncloud/ocis/v2/services/graph/mocks" + "github.com/owncloud/ocis/v2/services/graph/pkg/config" + "github.com/owncloud/ocis/v2/services/graph/pkg/config/defaults" + identitymocks "github.com/owncloud/ocis/v2/services/graph/pkg/identity/mocks" + service "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0" +) + +type applicationList struct { + Value []*libregraph.Application +} + +var _ = Describe("Applications", func() { + var ( + svc service.Service + ctx context.Context + cfg *config.Config + gatewayClient *mocks.GatewayClient + eventsPublisher mocks.Publisher + roleService *mocks.RoleService + identityBackend *identitymocks.Backend + + rr *httptest.ResponseRecorder + ) + + BeforeEach(func() { + eventsPublisher.On("Publish", mock.Anything, mock.Anything, mock.Anything).Return(nil) + + identityBackend = &identitymocks.Backend{} + roleService = &mocks.RoleService{} + gatewayClient = &mocks.GatewayClient{} + + rr = httptest.NewRecorder() + ctx = context.Background() + + cfg = defaults.FullDefaultConfig() + cfg.Identity.LDAP.CACert = "" // skip the startup checks, we don't use LDAP at all in this tests + cfg.TokenManager.JWTSecret = "loremipsum" + cfg.Commons = &shared.Commons{} + cfg.GRPCClientTLS = &shared.GRPCClientTLS{} + cfg.Application.ID = "some-application-ID" + + _ = ogrpc.Configure(ogrpc.GetClientOptions(cfg.GRPCClientTLS)...) + svc, _ = service.NewService( + service.Config(cfg), + service.WithGatewayClient(gatewayClient), + service.EventsPublisher(&eventsPublisher), + service.WithIdentityBackend(identityBackend), + service.WithRoleService(roleService), + ) + }) + + Describe("ListApplications", func() { + It("lists the configured application with appRoles", func() { + roleService.On("ListRoles", mock.Anything, mock.Anything, mock.Anything).Return(&settings.ListBundlesResponse{ + Bundles: []*settingsmsg.Bundle{ + { + Id: "some-appRole-ID", + Type: settingsmsg.Bundle_TYPE_ROLE, + DisplayName: "A human readable name for a role", + }, + }, + }, nil) + + r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/applications", nil) + svc.ListApplications(rr, r) + + Expect(rr.Code).To(Equal(http.StatusOK)) + + data, err := io.ReadAll(rr.Body) + Expect(err).ToNot(HaveOccurred()) + + responseList := applicationList{} + err = json.Unmarshal(data, &responseList) + Expect(err).ToNot(HaveOccurred()) + Expect(len(responseList.Value)).To(Equal(1)) + Expect(responseList.Value[0].Id).To(Equal(cfg.Application.ID)) + Expect(len(responseList.Value[0].GetAppRoles())).To(Equal(1)) + Expect(responseList.Value[0].GetAppRoles()[0].GetId()).To(Equal("some-appRole-ID")) + Expect(responseList.Value[0].GetAppRoles()[0].GetDisplayName()).To(Equal("A human readable name for a role")) + }) + }) + + Describe("GetApplication", func() { + It("gets the application with appRoles", func() { + roleService.On("ListRoles", mock.Anything, mock.Anything, mock.Anything).Return(&settings.ListBundlesResponse{ + Bundles: []*settingsmsg.Bundle{ + { + Id: "some-appRole-ID", + Type: settingsmsg.Bundle_TYPE_ROLE, + DisplayName: "A human readable name for a role", + }, + }, + }, nil) + + r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/applications/some-application-ID", nil) + rctx := chi.NewRouteContext() + rctx.URLParams.Add("applicationID", cfg.Application.ID) + r = r.WithContext(context.WithValue(ctx, chi.RouteCtxKey, rctx)) + svc.GetApplication(rr, r) + + Expect(rr.Code).To(Equal(http.StatusOK)) + + data, err := io.ReadAll(rr.Body) + Expect(err).ToNot(HaveOccurred()) + + application := libregraph.Application{} + err = json.Unmarshal(data, &application) + Expect(err).ToNot(HaveOccurred()) + Expect(application.Id).To(Equal(cfg.Application.ID)) + Expect(len(application.GetAppRoles())).To(Equal(1)) + Expect(application.GetAppRoles()[0].GetId()).To(Equal("some-appRole-ID")) + Expect(application.GetAppRoles()[0].GetDisplayName()).To(Equal("A human readable name for a role")) + }) + }) + +}) diff --git a/services/graph/pkg/service/v0/approleassignments.go b/services/graph/pkg/service/v0/approleassignments.go new file mode 100644 index 00000000000..849193cd61b --- /dev/null +++ b/services/graph/pkg/service/v0/approleassignments.go @@ -0,0 +1,129 @@ +package svc + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" + libregraph "github.com/owncloud/libre-graph-api-go" + settingsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/settings/v0" + settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0" + "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode" +) + +const principalTypeUser = "User" + +// ListAppRoleAssignments implements the Service interface. +func (g Graph) ListAppRoleAssignments(w http.ResponseWriter, r *http.Request) { + logger := g.logger.SubloggerWithRequestID(r.Context()) + logger.Info().Interface("query", r.URL.Query()).Msg("calling list appRoleAssignments") + + userID := chi.URLParam(r, "userID") + + lrar, err := g.roleService.ListRoleAssignments(r.Context(), &settingssvc.ListRoleAssignmentsRequest{ + AccountUuid: userID, + }) + if err != nil { + // TODO check the error type and return proper error code + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) + return + } + + values := make([]libregraph.AppRoleAssignment, 0, len(lrar.GetAssignments())) + for _, assignment := range lrar.GetAssignments() { + values = append(values, g.assignmentToAppRoleAssignment(assignment)) + } + + render.Status(r, http.StatusOK) + render.JSON(w, r, &ListResponse{Value: values}) +} + +// CreateAppRoleAssignment implements the Service interface. +func (g Graph) CreateAppRoleAssignment(w http.ResponseWriter, r *http.Request) { + logger := g.logger.SubloggerWithRequestID(r.Context()) + logger.Info().Interface("query", r.URL.Query()).Msg("calling create appRoleAssignment") + + appRoleAssignment := libregraph.NewAppRoleAssignmentWithDefaults() + err := json.NewDecoder(r.Body).Decode(appRoleAssignment) + if err != nil { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, fmt.Sprintf("invalid request body: %v", err.Error())) + return + } + + userID := chi.URLParam(r, "userID") + + if appRoleAssignment.GetPrincipalId() != userID { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, fmt.Sprintf("user id %s does not match principal id %v", userID, appRoleAssignment.GetPrincipalId())) + return + } + if appRoleAssignment.GetResourceId() != g.config.Application.ID { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, fmt.Sprintf("resource id %s does not match expected application id %v", userID, g.config.Application.ID)) + return + } + + artur, err := g.roleService.AssignRoleToUser(r.Context(), &settingssvc.AssignRoleToUserRequest{ + AccountUuid: userID, + RoleId: appRoleAssignment.AppRoleId, + }) + if err != nil { + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) + return + } + + render.Status(r, http.StatusCreated) + render.JSON(w, r, g.assignmentToAppRoleAssignment(artur.GetAssignment())) +} + +// DeleteAppRoleAssignment implements the Service interface. +func (g Graph) DeleteAppRoleAssignment(w http.ResponseWriter, r *http.Request) { + logger := g.logger.SubloggerWithRequestID(r.Context()) + logger.Info().Interface("body", r.Body).Msg("calling delete appRoleAssignment") + + userID := chi.URLParam(r, "userID") + + // check assignment belongs to the user + lrar, err := g.roleService.ListRoleAssignments(r.Context(), &settingssvc.ListRoleAssignmentsRequest{ + AccountUuid: userID, + }) + if err != nil { + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) + return + } + + appRoleAssignmentID := chi.URLParam(r, "appRoleAssignmentID") + + assignmentFound := false + for _, roleAssignment := range lrar.GetAssignments() { + if roleAssignment.Id == appRoleAssignmentID { + assignmentFound = true + } + } + if !assignmentFound { + errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, fmt.Sprintf("appRoleAssignment %v not found for user %v", appRoleAssignmentID, userID)) + return + } + + _, err = g.roleService.RemoveRoleFromUser(r.Context(), &settingssvc.RemoveRoleFromUserRequest{ + Id: appRoleAssignmentID, + }) + if err != nil { + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) + return + } + + render.NoContent(w, r) +} + +func (g Graph) assignmentToAppRoleAssignment(assignment *settingsmsg.UserRoleAssignment) libregraph.AppRoleAssignment { + appRoleAssignment := libregraph.NewAppRoleAssignmentWithDefaults() + appRoleAssignment.SetId(assignment.Id) + appRoleAssignment.SetAppRoleId(assignment.RoleId) + appRoleAssignment.SetPrincipalType(principalTypeUser) // currently always assigned to the user + appRoleAssignment.SetResourceId(g.config.Application.ID) + appRoleAssignment.SetResourceDisplayName(g.config.Application.DisplayName) + appRoleAssignment.SetPrincipalId(assignment.AccountUuid) + // appRoleAssignment.SetPrincipalDisplayName() // TODO fetch and cache + return *appRoleAssignment +} diff --git a/services/graph/pkg/service/v0/approleassignments_test.go b/services/graph/pkg/service/v0/approleassignments_test.go new file mode 100644 index 00000000000..807bbc1ceda --- /dev/null +++ b/services/graph/pkg/service/v0/approleassignments_test.go @@ -0,0 +1,198 @@ +package svc_test + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + + userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + revactx "github.com/cs3org/reva/v2/pkg/ctx" + "github.com/go-chi/chi/v5" + "github.com/golang/protobuf/ptypes/empty" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/stretchr/testify/mock" + + libregraph "github.com/owncloud/libre-graph-api-go" + ogrpc "github.com/owncloud/ocis/v2/ocis-pkg/service/grpc" + "github.com/owncloud/ocis/v2/ocis-pkg/shared" + settingsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/settings/v0" + settings "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0" + "github.com/owncloud/ocis/v2/services/graph/mocks" + "github.com/owncloud/ocis/v2/services/graph/pkg/config" + "github.com/owncloud/ocis/v2/services/graph/pkg/config/defaults" + identitymocks "github.com/owncloud/ocis/v2/services/graph/pkg/identity/mocks" + service "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0" +) + +type assignmentList struct { + Value []*libregraph.AppRoleAssignment +} + +var _ = Describe("AppRoleAssignments", func() { + var ( + svc service.Service + ctx context.Context + cfg *config.Config + gatewayClient *mocks.GatewayClient + eventsPublisher mocks.Publisher + roleService *mocks.RoleService + identityBackend *identitymocks.Backend + + rr *httptest.ResponseRecorder + + currentUser = &userv1beta1.User{ + Id: &userv1beta1.UserId{ + OpaqueId: "user", + }, + } + ) + + BeforeEach(func() { + eventsPublisher.On("Publish", mock.Anything, mock.Anything, mock.Anything).Return(nil) + + identityBackend = &identitymocks.Backend{} + roleService = &mocks.RoleService{} + gatewayClient = &mocks.GatewayClient{} + + rr = httptest.NewRecorder() + ctx = context.Background() + + cfg = defaults.FullDefaultConfig() + cfg.Identity.LDAP.CACert = "" // skip the startup checks, we don't use LDAP at all in this tests + cfg.TokenManager.JWTSecret = "loremipsum" + cfg.Commons = &shared.Commons{} + cfg.GRPCClientTLS = &shared.GRPCClientTLS{} + cfg.Application.ID = "some-application-ID" + + _ = ogrpc.Configure(ogrpc.GetClientOptions(cfg.GRPCClientTLS)...) + svc, _ = service.NewService( + service.Config(cfg), + service.WithGatewayClient(gatewayClient), + service.EventsPublisher(&eventsPublisher), + service.WithIdentityBackend(identityBackend), + service.WithRoleService(roleService), + ) + }) + + Describe("ListAppRoleAssignments", func() { + It("lists the appRoleAssignments", func() { + user := &libregraph.User{ + Id: libregraph.PtrString("user1"), + } + assignments := []*settingsmsg.UserRoleAssignment{ + { + Id: "some-appRoleAssignment-ID", + AccountUuid: user.GetId(), + RoleId: "some-appRole-ID", + }, + } + roleService.On("ListRoleAssignments", mock.Anything, mock.Anything, mock.Anything).Return(&settings.ListRoleAssignmentsResponse{Assignments: assignments}, nil) + + r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/users/user1/appRoleAssignments", nil) + rctx := chi.NewRouteContext() + rctx.URLParams.Add("userID", user.GetId()) + r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) + svc.ListAppRoleAssignments(rr, r) + + Expect(rr.Code).To(Equal(http.StatusOK)) + + data, err := io.ReadAll(rr.Body) + Expect(err).ToNot(HaveOccurred()) + + responseList := assignmentList{} + err = json.Unmarshal(data, &responseList) + Expect(err).ToNot(HaveOccurred()) + Expect(len(responseList.Value)).To(Equal(1)) + Expect(responseList.Value[0].GetId()).ToNot(BeEmpty()) + Expect(responseList.Value[0].GetAppRoleId()).To(Equal("some-appRole-ID")) + Expect(responseList.Value[0].GetPrincipalId()).To(Equal(user.GetId())) + Expect(responseList.Value[0].GetResourceId()).To(Equal(cfg.Application.ID)) + + }) + + }) + + Describe("CreateAppRoleAssignment", func() { + It("creates an appRoleAssignment", func() { + user := &libregraph.User{ + Id: libregraph.PtrString("user1"), + } + userRoleAssignment := &settingsmsg.UserRoleAssignment{ + Id: "some-appRoleAssignment-ID", + AccountUuid: user.GetId(), + RoleId: "some-appRole-ID", + } + roleService.On("AssignRoleToUser", mock.Anything, mock.Anything, mock.Anything).Return(&settings.AssignRoleToUserResponse{Assignment: userRoleAssignment}, nil) + + ara := libregraph.NewAppRoleAssignmentWithDefaults() + ara.SetAppRoleId("some-appRole-ID") + ara.SetPrincipalId(user.GetId()) + ara.SetResourceId(cfg.Application.ID) + + araJson, err := json.Marshal(ara) + Expect(err).ToNot(HaveOccurred()) + + r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/users/user1/appRoleAssignments", bytes.NewBuffer(araJson)) + rctx := chi.NewRouteContext() + rctx.URLParams.Add("userID", user.GetId()) + r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) + svc.CreateAppRoleAssignment(rr, r) + + Expect(rr.Code).To(Equal(http.StatusCreated)) + + data, err := io.ReadAll(rr.Body) + Expect(err).ToNot(HaveOccurred()) + + assignment := libregraph.AppRoleAssignment{} + err = json.Unmarshal(data, &assignment) + Expect(err).ToNot(HaveOccurred()) + Expect(assignment.GetId()).ToNot(BeEmpty()) + Expect(assignment.GetAppRoleId()).To(Equal("some-appRole-ID")) + Expect(assignment.GetPrincipalId()).To(Equal("user1")) + Expect(assignment.GetResourceId()).To(Equal(cfg.Application.ID)) + }) + + }) + + Describe("DeleteAppRoleAssignment", func() { + It("deletes an appRoleAssignment", func() { + user := &libregraph.User{ + Id: libregraph.PtrString("user1"), + } + + assignments := []*settingsmsg.UserRoleAssignment{ + { + Id: "some-appRoleAssignment-ID", + AccountUuid: user.GetId(), + RoleId: "some-appRole-ID", + }, + } + roleService.On("ListRoleAssignments", mock.Anything, mock.Anything, mock.Anything).Return(&settings.ListRoleAssignmentsResponse{Assignments: assignments}, nil) + + roleService.On("RemoveRoleFromUser", mock.Anything, mock.Anything, mock.Anything).Return(&empty.Empty{}, nil) + + ara := libregraph.NewAppRoleAssignmentWithDefaults() + ara.SetAppRoleId("some-appRole-ID") + ara.SetPrincipalId(user.GetId()) + ara.SetResourceId(cfg.Application.ID) + + araJson, err := json.Marshal(ara) + Expect(err).ToNot(HaveOccurred()) + + r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/users/user1/appRoleAssignments/some-appRoleAssignment-ID", bytes.NewBuffer(araJson)) + rctx := chi.NewRouteContext() + rctx.URLParams.Add("userID", user.GetId()) + rctx.URLParams.Add("appRoleAssignmentID", "some-appRoleAssignment-ID") + r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) + svc.DeleteAppRoleAssignment(rr, r) + + Expect(rr.Code).To(Equal(http.StatusNoContent)) + + }) + + }) +}) diff --git a/services/graph/pkg/service/v0/educationuser_test.go b/services/graph/pkg/service/v0/educationuser_test.go index adc45de6e19..1e9e445cbb9 100644 --- a/services/graph/pkg/service/v0/educationuser_test.go +++ b/services/graph/pkg/service/v0/educationuser_test.go @@ -74,7 +74,7 @@ var _ = Describe("EducationUsers", func() { service.WithGatewayClient(gatewayClient), service.EventsPublisher(&eventsPublisher), service.WithIdentityEducationBackend(identityEducationBackend), - //service.WithRoleService(roleService), + service.WithRoleService(roleService), ) }) diff --git a/services/graph/pkg/service/v0/instrument.go b/services/graph/pkg/service/v0/instrument.go index eafb980fef9..284fb7a4687 100644 --- a/services/graph/pkg/service/v0/instrument.go +++ b/services/graph/pkg/service/v0/instrument.go @@ -24,6 +24,16 @@ func (i instrument) ServeHTTP(w http.ResponseWriter, r *http.Request) { i.next.ServeHTTP(w, r) } +// ListApplications implements the Service interface. +func (i instrument) ListApplications(w http.ResponseWriter, r *http.Request) { + i.next.ListApplications(w, r) +} + +// GetApplication implements the Service interface. +func (i instrument) GetApplication(w http.ResponseWriter, r *http.Request) { + i.next.GetApplication(w, r) +} + // GetMe implements the Service interface. func (i instrument) GetMe(w http.ResponseWriter, r *http.Request) { i.next.GetMe(w, r) @@ -59,6 +69,21 @@ func (i instrument) ChangeOwnPassword(w http.ResponseWriter, r *http.Request) { i.next.ChangeOwnPassword(w, r) } +// ListAppRoleAssignments implements the Service interface. +func (i instrument) ListAppRoleAssignments(w http.ResponseWriter, r *http.Request) { + i.next.ListAppRoleAssignments(w, r) +} + +// CreateAppRoleAssignment implements the Service interface. +func (i instrument) CreateAppRoleAssignment(w http.ResponseWriter, r *http.Request) { + i.next.CreateAppRoleAssignment(w, r) +} + +// DeleteAppRoleAssignment implements the Service interface. +func (i instrument) DeleteAppRoleAssignment(w http.ResponseWriter, r *http.Request) { + i.next.DeleteAppRoleAssignment(w, r) +} + // GetGroups implements the Service interface. func (i instrument) GetGroups(w http.ResponseWriter, r *http.Request) { i.next.GetGroups(w, r) diff --git a/services/graph/pkg/service/v0/logging.go b/services/graph/pkg/service/v0/logging.go index 1e3369b6ce1..62547bf6be1 100644 --- a/services/graph/pkg/service/v0/logging.go +++ b/services/graph/pkg/service/v0/logging.go @@ -24,6 +24,16 @@ func (l logging) ServeHTTP(w http.ResponseWriter, r *http.Request) { l.next.ServeHTTP(w, r) } +// ListApplications implements the Service interface. +func (l logging) ListApplications(w http.ResponseWriter, r *http.Request) { + l.next.ListApplications(w, r) +} + +// GetApplication implements the Service interface. +func (l logging) GetApplication(w http.ResponseWriter, r *http.Request) { + l.next.GetApplication(w, r) +} + // GetMe implements the Service interface. func (l logging) GetMe(w http.ResponseWriter, r *http.Request) { l.next.GetMe(w, r) @@ -59,6 +69,21 @@ func (l logging) ChangeOwnPassword(w http.ResponseWriter, r *http.Request) { l.next.ChangeOwnPassword(w, r) } +// ListAppRoleAssignments implements the Service interface. +func (l logging) ListAppRoleAssignments(w http.ResponseWriter, r *http.Request) { + l.next.ListAppRoleAssignments(w, r) +} + +// CreateAppRoleAssignment implements the Service interface. +func (l logging) CreateAppRoleAssignment(w http.ResponseWriter, r *http.Request) { + l.next.CreateAppRoleAssignment(w, r) +} + +// DeleteAppRoleAssignment implements the Service interface. +func (l logging) DeleteAppRoleAssignment(w http.ResponseWriter, r *http.Request) { + l.next.DeleteAppRoleAssignment(w, r) +} + // GetGroups implements the Service interface. func (l logging) GetGroups(w http.ResponseWriter, r *http.Request) { l.next.GetGroups(w, r) diff --git a/services/graph/pkg/service/v0/service.go b/services/graph/pkg/service/v0/service.go index 98ce2d1a27a..3a4b99dac7a 100644 --- a/services/graph/pkg/service/v0/service.go +++ b/services/graph/pkg/service/v0/service.go @@ -32,6 +32,10 @@ const ( // Service defines the service handlers. type Service interface { ServeHTTP(http.ResponseWriter, *http.Request) + + ListApplications(w http.ResponseWriter, r *http.Request) + GetApplication(http.ResponseWriter, *http.Request) + GetMe(http.ResponseWriter, *http.Request) GetUsers(http.ResponseWriter, *http.Request) GetUser(http.ResponseWriter, *http.Request) @@ -40,6 +44,10 @@ type Service interface { PatchUser(http.ResponseWriter, *http.Request) ChangeOwnPassword(http.ResponseWriter, *http.Request) + ListAppRoleAssignments(http.ResponseWriter, *http.Request) + CreateAppRoleAssignment(http.ResponseWriter, *http.Request) + DeleteAppRoleAssignment(http.ResponseWriter, *http.Request) + GetGroups(http.ResponseWriter, *http.Request) GetGroup(http.ResponseWriter, *http.Request) PostGroup(http.ResponseWriter, *http.Request) @@ -118,80 +126,8 @@ func NewService(opts ...Option) (Graph, error) { identityEducationBackend: options.IdentityEducationBackend, } - if options.IdentityBackend == nil { - switch options.Config.Identity.Backend { - case "cs3": - svc.identityBackend = &identity.CS3{ - Config: options.Config.Reva, - Logger: &options.Logger, - } - case "ldap": - var err error - - var tlsConf *tls.Config - if options.Config.Identity.LDAP.Insecure { - // When insecure is set to true then we don't need a certificate. - options.Config.Identity.LDAP.CACert = "" - tlsConf = &tls.Config{ - MinVersion: tls.VersionTLS12, - //nolint:gosec // We need the ability to run with "insecure" (dev/testing) - InsecureSkipVerify: options.Config.Identity.LDAP.Insecure, - } - } - - if options.Config.Identity.LDAP.CACert != "" { - if err := ocisldap.WaitForCA(options.Logger, - options.Config.Identity.LDAP.Insecure, - options.Config.Identity.LDAP.CACert); err != nil { - options.Logger.Fatal().Err(err).Msg("The configured LDAP CA cert does not exist") - } - if tlsConf == nil { - tlsConf = &tls.Config{ - MinVersion: tls.VersionTLS12, - } - } - certs := x509.NewCertPool() - pemData, err := os.ReadFile(options.Config.Identity.LDAP.CACert) - if err != nil { - options.Logger.Error().Err(err).Msgf("Error initializing LDAP Backend") - return svc, err - } - if !certs.AppendCertsFromPEM(pemData) { - options.Logger.Error().Msgf("Error initializing LDAP Backend. Adding CA cert failed") - return svc, err - } - tlsConf.RootCAs = certs - } - - conn := ldap.NewLDAPWithReconnect(&options.Logger, - ldap.Config{ - URI: options.Config.Identity.LDAP.URI, - BindDN: options.Config.Identity.LDAP.BindDN, - BindPassword: options.Config.Identity.LDAP.BindPassword, - TLSConfig: tlsConf, - }, - ) - lb, err := identity.NewLDAPBackend(conn, options.Config.Identity.LDAP, &options.Logger) - if err != nil { - options.Logger.Error().Msgf("Error initializing LDAP Backend: '%s'", err) - return svc, err - } - svc.identityBackend = lb - if options.IdentityEducationBackend == nil { - if options.Config.Identity.LDAP.EducationResourcesEnabled { - svc.identityEducationBackend = lb - } else { - errEduBackend := &identity.ErrEducationBackend{} - svc.identityEducationBackend = errEduBackend - } - } - default: - err := fmt.Errorf("Unknown Identity Backend: '%s'", options.Config.Identity.Backend) - options.Logger.Err(err) - return svc, err - } - } else { - svc.identityBackend = options.IdentityBackend + if err := setIdentityBackends(options, &svc); err != nil { + return svc, err } if options.PermissionService == nil { @@ -200,6 +136,12 @@ func NewService(opts ...Option) (Graph, error) { svc.permissionsService = options.PermissionService } + if options.RoleService == nil { + svc.roleService = settingssvc.NewRoleService("com.owncloud.api.settings", grpc.DefaultClient()) + } else { + svc.roleService = options.RoleService + } + roleManager := options.RoleManager if roleManager == nil { storeOptions := store.OcisStoreOptions{ @@ -230,6 +172,10 @@ func NewService(opts ...Option) (Graph, error) { r.Put("/tags", svc.AssignTags) r.Delete("/tags", svc.UnassignTags) }) + r.Route("/applications", func(r chi.Router) { + r.Get("/", svc.ListApplications) + r.Get("/{applicationID}", svc.GetApplication) + }) r.Route("/me", func(r chi.Router) { r.Get("/", svc.GetMe) r.Get("/drives", svc.GetDrives) @@ -243,6 +189,11 @@ func NewService(opts ...Option) (Graph, error) { r.Get("/", svc.GetUser) r.With(requireAdmin).Delete("/", svc.DeleteUser) r.With(requireAdmin).Patch("/", svc.PatchUser) + r.With(requireAdmin).Route("/appRoleAssignments", func(r chi.Router) { + r.Get("/", svc.ListAppRoleAssignments) + r.Post("/", svc.CreateAppRoleAssignment) + r.Delete("/{appRoleAssignmentID}", svc.DeleteAppRoleAssignment) + }) }) }) r.Route("/groups", func(r chi.Router) { @@ -313,6 +264,88 @@ func NewService(opts ...Option) (Graph, error) { return svc, nil } +func setIdentityBackends(options Options, svc *Graph) error { + if options.IdentityBackend == nil { + switch options.Config.Identity.Backend { + case "cs3": + svc.identityBackend = &identity.CS3{ + Config: options.Config.Reva, + Logger: &options.Logger, + } + case "ldap": + var err error + + var tlsConf *tls.Config + if options.Config.Identity.LDAP.Insecure { + + // When insecure is set to true then we don't need a certificate. + options.Config.Identity.LDAP.CACert = "" + tlsConf = &tls.Config{ + MinVersion: tls.VersionTLS12, + + //nolint:gosec // We need the ability to run with "insecure" (dev/testing) + InsecureSkipVerify: options.Config.Identity.LDAP.Insecure, + } + } + + if options.Config.Identity.LDAP.CACert != "" { + if err := ocisldap.WaitForCA(options.Logger, + options.Config.Identity.LDAP.Insecure, + options.Config.Identity.LDAP.CACert); err != nil { + options.Logger.Fatal().Err(err).Msg("The configured LDAP CA cert does not exist") + } + if tlsConf == nil { + tlsConf = &tls.Config{ + MinVersion: tls.VersionTLS12, + } + } + certs := x509.NewCertPool() + pemData, err := os.ReadFile(options.Config.Identity.LDAP.CACert) + if err != nil { + options.Logger.Error().Err(err).Msg("Error initializing LDAP Backend") + return err + } + if !certs.AppendCertsFromPEM(pemData) { + options.Logger.Error().Msg("Error initializing LDAP Backend. Adding CA cert failed") + return err + } + tlsConf.RootCAs = certs + } + + conn := ldap.NewLDAPWithReconnect(&options.Logger, + ldap.Config{ + URI: options.Config.Identity.LDAP.URI, + BindDN: options.Config.Identity.LDAP.BindDN, + BindPassword: options.Config.Identity.LDAP.BindPassword, + TLSConfig: tlsConf, + }, + ) + lb, err := identity.NewLDAPBackend(conn, options.Config.Identity.LDAP, &options.Logger) + if err != nil { + options.Logger.Error().Err(err).Msg("Error initializing LDAP Backend") + return err + } + svc.identityBackend = lb + if options.IdentityEducationBackend == nil { + if options.Config.Identity.LDAP.EducationResourcesEnabled { + svc.identityEducationBackend = lb + } else { + errEduBackend := &identity.ErrEducationBackend{} + svc.identityEducationBackend = errEduBackend + } + } + default: + err := fmt.Errorf("unknown identity backend: '%s'", options.Config.Identity.Backend) + options.Logger.Err(err) + return err + } + } else { + svc.identityBackend = options.IdentityBackend + } + + return nil +} + // parseHeaderPurge parses the 'Purge' header. // '1', 't', 'T', 'TRUE', 'true', 'True' are parsed as true // all other values are false. diff --git a/services/graph/pkg/service/v0/tracing.go b/services/graph/pkg/service/v0/tracing.go index 160289f7416..65a57807da9 100644 --- a/services/graph/pkg/service/v0/tracing.go +++ b/services/graph/pkg/service/v0/tracing.go @@ -20,6 +20,16 @@ func (t tracing) ServeHTTP(w http.ResponseWriter, r *http.Request) { t.next.ServeHTTP(w, r) } +// ListApplications implements the Service interface. +func (t tracing) ListApplications(w http.ResponseWriter, r *http.Request) { + t.next.ListApplications(w, r) +} + +// GetApplication implements the Service interface. +func (t tracing) GetApplication(w http.ResponseWriter, r *http.Request) { + t.next.GetApplication(w, r) +} + // GetMe implements the Service interface. func (t tracing) GetMe(w http.ResponseWriter, r *http.Request) { t.next.GetMe(w, r) @@ -55,6 +65,21 @@ func (t tracing) ChangeOwnPassword(w http.ResponseWriter, r *http.Request) { t.next.ChangeOwnPassword(w, r) } +// ListAppRoleAssignments implements the Service interface. +func (t tracing) ListAppRoleAssignments(w http.ResponseWriter, r *http.Request) { + t.next.ListAppRoleAssignments(w, r) +} + +// CreateAppRoleAssignment implements the Service interface. +func (t tracing) CreateAppRoleAssignment(w http.ResponseWriter, r *http.Request) { + t.next.CreateAppRoleAssignment(w, r) +} + +// DeleteAppRoleAssignment implements the Service interface. +func (t tracing) DeleteAppRoleAssignment(w http.ResponseWriter, r *http.Request) { + t.next.DeleteAppRoleAssignment(w, r) +} + // GetGroups implements the Service interface. func (t tracing) GetGroups(w http.ResponseWriter, r *http.Request) { t.next.GetGroups(w, r) diff --git a/services/graph/pkg/service/v0/users.go b/services/graph/pkg/service/v0/users.go index 31c390c87e5..b37180b12d5 100644 --- a/services/graph/pkg/service/v0/users.go +++ b/services/graph/pkg/service/v0/users.go @@ -58,6 +58,30 @@ func (g Graph) GetMe(w http.ResponseWriter, r *http.Request) { return } } + + // expand appRoleAssignments if requested + if slices.Contains(exp, "appRoleAssignments") { + lrar, err := g.roleService.ListRoleAssignments(r.Context(), &settings.ListRoleAssignmentsRequest{ + AccountUuid: me.GetId(), + }) + if err != nil { + logger.Debug().Err(err).Str("userid", me.GetId()).Msg("could not get appRoleAssignments for self") + var errcode errorcode.Error + if errors.As(err, &errcode) { + errcode.Render(w, r) + } else { + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) + } + return + } + + values := make([]libregraph.AppRoleAssignment, 0, len(lrar.GetAssignments())) + for _, assignment := range lrar.GetAssignments() { + values = append(values, g.assignmentToAppRoleAssignment(assignment)) + } + me.AppRoleAssignments = values + } + render.Status(r, http.StatusOK) render.JSON(w, r, me) } @@ -87,6 +111,26 @@ func (g Graph) GetUsers(w http.ResponseWriter, r *http.Request) { return } + // expand appRoleAssignments if requested + exp := strings.Split(r.URL.Query().Get("$expand"), ",") + if slices.Contains(exp, "appRoleAssignments") { + for _, u := range users { + lrar, err := g.roleService.ListRoleAssignments(r.Context(), &settings.ListRoleAssignmentsRequest{ + AccountUuid: u.GetId(), + }) + if err != nil { + logger.Debug().Err(err).Str("userid", u.GetId()).Msg("could not get appRoleAssignments when listing user") + continue + } + + values := make([]libregraph.AppRoleAssignment, 0, len(lrar.GetAssignments())) + for _, assignment := range lrar.GetAssignments() { + values = append(values, g.assignmentToAppRoleAssignment(assignment)) + } + u.AppRoleAssignments = values + } + } + users, err = sortUsers(odataReq, users) if err != nil { logger.Debug().Interface("query", odataReq).Msg("error while sorting users according to query") @@ -274,6 +318,29 @@ func (g Graph) GetUser(w http.ResponseWriter, r *http.Request) { } } } + // expand appRoleAssignments if requested + if slices.Contains(exp, "appRoleAssignments") { + + lrar, err := g.roleService.ListRoleAssignments(r.Context(), &settings.ListRoleAssignmentsRequest{ + AccountUuid: user.GetId(), + }) + if err != nil { + logger.Debug().Err(err).Str("userid", user.GetId()).Msg("could not get appRoleAssignments for user") + var errcode errorcode.Error + if errors.As(err, &errcode) { + errcode.Render(w, r) + } else { + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) + } + return + } + + values := make([]libregraph.AppRoleAssignment, 0, len(lrar.GetAssignments())) + for _, assignment := range lrar.GetAssignments() { + values = append(values, g.assignmentToAppRoleAssignment(assignment)) + } + user.AppRoleAssignments = values + } render.Status(r, http.StatusOK) render.JSON(w, r, user) diff --git a/services/graph/pkg/service/v0/users_test.go b/services/graph/pkg/service/v0/users_test.go index 070f547d816..77a7f48b0e8 100644 --- a/services/graph/pkg/service/v0/users_test.go +++ b/services/graph/pkg/service/v0/users_test.go @@ -17,11 +17,13 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/stretchr/testify/mock" + "go-micro.dev/v4/client" libregraph "github.com/owncloud/libre-graph-api-go" ogrpc "github.com/owncloud/ocis/v2/ocis-pkg/service/grpc" "github.com/owncloud/ocis/v2/ocis-pkg/shared" - settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0" + settingsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/settings/v0" + settings "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0" "github.com/owncloud/ocis/v2/services/graph/mocks" "github.com/owncloud/ocis/v2/services/graph/pkg/config" "github.com/owncloud/ocis/v2/services/graph/pkg/config/defaults" @@ -67,6 +69,7 @@ var _ = Describe("Users", func() { cfg.TokenManager.JWTSecret = "loremipsum" cfg.Commons = &shared.Commons{} cfg.GRPCClientTLS = &shared.GRPCClientTLS{} + cfg.Application.ID = "some-application-ID" _ = ogrpc.Configure(ogrpc.GetClientOptions(cfg.GRPCClientTLS)...) svc, _ = service.NewService( @@ -94,8 +97,13 @@ var _ = Describe("Users", func() { Expect(rr.Code).To(Equal(http.StatusOK)) }) - It("expands the user", func() { - user := &libregraph.User{} + It("expands the memberOf", func() { + user := &libregraph.User{ + Id: libregraph.PtrString("user1"), + MemberOf: []libregraph.Group{ + {DisplayName: libregraph.PtrString("somegroup")}, + }, + } identityBackend.On("GetUser", mock.Anything, mock.Anything, mock.Anything).Return(user, nil) r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me?$expand=memberOf", nil) @@ -103,6 +111,48 @@ var _ = Describe("Users", func() { svc.GetMe(rr, r) Expect(rr.Code).To(Equal(http.StatusOK)) + data, err := io.ReadAll(rr.Body) + Expect(err).ToNot(HaveOccurred()) + + responseUser := &libregraph.User{} + err = json.Unmarshal(data, &responseUser) + Expect(err).ToNot(HaveOccurred()) + + Expect(responseUser.GetId()).To(Equal("user1")) + Expect(responseUser.GetMemberOf()).To(HaveLen(1)) + Expect(responseUser.GetMemberOf()[0].GetDisplayName()).To(Equal("somegroup")) + + }) + + It("expands the appRoleAssignments", func() { + assignments := []*settingsmsg.UserRoleAssignment{ + { + Id: "some-appRoleAssignment-ID", + AccountUuid: "user", + RoleId: "some-appRole-ID", + }, + } + roleService.On("ListRoleAssignments", mock.Anything, mock.Anything, mock.Anything).Return(&settings.ListRoleAssignmentsResponse{Assignments: assignments}, nil) + + r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me?$expand=appRoleAssignments", nil) + r = r.WithContext(revactx.ContextSetUser(ctx, currentUser)) + svc.GetMe(rr, r) + + Expect(rr.Code).To(Equal(http.StatusOK)) + + data, err := io.ReadAll(rr.Body) + Expect(err).ToNot(HaveOccurred()) + + responseUser := &libregraph.User{} + err = json.Unmarshal(data, &responseUser) + Expect(err).ToNot(HaveOccurred()) + + Expect(responseUser.GetId()).To(Equal("user")) + Expect(responseUser.GetAppRoleAssignments()).To(HaveLen(1)) + Expect(responseUser.GetAppRoleAssignments()[0].GetId()).To(Equal("some-appRoleAssignment-ID")) + Expect(responseUser.GetAppRoleAssignments()[0].GetAppRoleId()).To(Equal("some-appRole-ID")) + Expect(responseUser.GetAppRoleAssignments()[0].GetPrincipalId()).To(Equal("user")) + Expect(responseUser.GetAppRoleAssignments()[0].GetResourceId()).To(Equal("some-application-ID")) }) }) @@ -216,6 +266,62 @@ var _ = Describe("Users", func() { Expect(rr.Code).To(Equal(http.StatusBadRequest)) }) + + It("expands the appRoleAssignments", func() { + + user := &libregraph.User{} + user.SetId("user1") + user.SetMail("z@example.com") + user.SetDisplayName("9") + user.SetOnPremisesSamAccountName("9") + user2 := &libregraph.User{} + user2.SetId("user2") + user2.SetMail("a@example.com") + user2.SetDisplayName("1") + user2.SetOnPremisesSamAccountName("1") + users := []*libregraph.User{user, user2} + identityBackend.On("GetUsers", mock.Anything, mock.Anything, mock.Anything).Return(users, nil) + + roleService.On("ListRoleAssignments", mock.Anything, mock.Anything, mock.Anything).Return(func(ctx context.Context, in *settings.ListRoleAssignmentsRequest, opts ...client.CallOption) *settings.ListRoleAssignmentsResponse { + return &settings.ListRoleAssignmentsResponse{Assignments: []*settingsmsg.UserRoleAssignment{ + { + Id: "some-appRoleAssignment-ID", + AccountUuid: in.GetAccountUuid(), + RoleId: "some-appRole-ID", + }, + }} + }, nil) + + r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/users?$expand=appRoleAssignments", nil) + r = r.WithContext(revactx.ContextSetUser(ctx, currentUser)) + svc.GetUsers(rr, r) + + Expect(rr.Code).To(Equal(http.StatusOK)) + + data, err := io.ReadAll(rr.Body) + Expect(err).ToNot(HaveOccurred()) + + res := userList{} + err = json.Unmarshal(data, &res) + Expect(err).ToNot(HaveOccurred()) + + responseUsers := res.Value + Expect(len(responseUsers)).To(Equal(2)) + Expect(responseUsers[0].GetId()).To(Equal("user1")) + Expect(responseUsers[0].GetAppRoleAssignments()).To(HaveLen(1)) + Expect(responseUsers[0].GetAppRoleAssignments()[0].GetId()).To(Equal("some-appRoleAssignment-ID")) + Expect(responseUsers[0].GetAppRoleAssignments()[0].GetAppRoleId()).To(Equal("some-appRole-ID")) + Expect(responseUsers[0].GetAppRoleAssignments()[0].GetPrincipalId()).To(Equal("user1")) + Expect(responseUsers[0].GetAppRoleAssignments()[0].GetResourceId()).To(Equal("some-application-ID")) + + Expect(responseUsers[1].GetId()).To(Equal("user2")) + Expect(responseUsers[1].GetAppRoleAssignments()).To(HaveLen(1)) + Expect(responseUsers[1].GetAppRoleAssignments()[0].GetId()).To(Equal("some-appRoleAssignment-ID")) + Expect(responseUsers[1].GetAppRoleAssignments()[0].GetAppRoleId()).To(Equal("some-appRole-ID")) + Expect(responseUsers[1].GetAppRoleAssignments()[0].GetPrincipalId()).To(Equal("user2")) + Expect(responseUsers[1].GetAppRoleAssignments()[0].GetResourceId()).To(Equal("some-application-ID")) + + }) }) Describe("GetUser", func() { @@ -322,6 +428,44 @@ var _ = Describe("Users", func() { Expect(responseUser.GetId()).To(Equal("user1")) Expect(len(responseUser.GetDrives())).To(Equal(1)) }) + + It("expands the appRoleAssignments", func() { + user := &libregraph.User{} + user.SetId("user1") + + identityBackend.On("GetUser", mock.Anything, mock.Anything, mock.Anything).Return(user, nil) + + assignments := []*settingsmsg.UserRoleAssignment{ + { + Id: "some-appRoleAssignment-ID", + AccountUuid: "user1", + RoleId: "some-appRole-ID", + }, + } + roleService.On("ListRoleAssignments", mock.Anything, mock.Anything, mock.Anything).Return(&settings.ListRoleAssignmentsResponse{Assignments: assignments}, nil) + + r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/users/user1?$expand=appRoleAssignments", nil) + rctx := chi.NewRouteContext() + rctx.URLParams.Add("userID", user.GetId()) + r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) + svc.GetUser(rr, r) + + Expect(rr.Code).To(Equal(http.StatusOK)) + + data, err := io.ReadAll(rr.Body) + Expect(err).ToNot(HaveOccurred()) + + responseUser := &libregraph.User{} + err = json.Unmarshal(data, &responseUser) + Expect(err).ToNot(HaveOccurred()) + + Expect(responseUser.GetId()).To(Equal("user1")) + Expect(responseUser.GetAppRoleAssignments()).To(HaveLen(1)) + Expect(responseUser.GetAppRoleAssignments()[0].GetId()).To(Equal("some-appRoleAssignment-ID")) + Expect(responseUser.GetAppRoleAssignments()[0].GetAppRoleId()).To(Equal("some-appRole-ID")) + Expect(responseUser.GetAppRoleAssignments()[0].GetPrincipalId()).To(Equal("user1")) + Expect(responseUser.GetAppRoleAssignments()[0].GetResourceId()).To(Equal("some-application-ID")) + }) }) Describe("PostUser", func() { @@ -381,7 +525,7 @@ var _ = Describe("Users", func() { }) It("creates a user", func() { - roleService.On("AssignRoleToUser", mock.Anything, mock.Anything).Return(&settingssvc.AssignRoleToUserResponse{}, nil) + roleService.On("AssignRoleToUser", mock.Anything, mock.Anything).Return(&settings.AssignRoleToUserResponse{}, nil) identityBackend.On("CreateUser", mock.Anything, mock.Anything).Return(func(ctx context.Context, user libregraph.User) *libregraph.User { user.SetId("/users/user") return &user diff --git a/services/settings/pkg/service/v0/service.go b/services/settings/pkg/service/v0/service.go index 0b9955381cd..e5db4683d70 100644 --- a/services/settings/pkg/service/v0/service.go +++ b/services/settings/pkg/service/v0/service.go @@ -79,7 +79,7 @@ func (g Service) CheckPermission(ctx context.Context, req *permissions.CheckPerm permission, err := g.manager.ReadPermissionByName(req.Permission, roleIDs) if err != nil { - if !errors.Is(err, settings.ErrPermissionNotFound) { + if !errors.Is(err, settings.ErrNotFound) { return &permissions.CheckPermissionResponse{ Status: status.NewInternal(ctx, err.Error()), }, nil diff --git a/services/settings/pkg/settings/settings.go b/services/settings/pkg/settings/settings.go index 737585834fc..6fefd82718e 100644 --- a/services/settings/pkg/settings/settings.go +++ b/services/settings/pkg/settings/settings.go @@ -12,7 +12,12 @@ var ( Registry = map[string]RegisterFunc{} // ErrPermissionNotFound defines a new error for when a permission was not found + // + // Deprecated use the more generic ErrNotFound ErrPermissionNotFound = errors.New("permission not found") + + // ErrNotFound is the error to use when a resource was not found. + ErrNotFound = errors.New("not found") ) // RegisterFunc stores store constructors diff --git a/services/settings/pkg/store/errortypes/errortypes.go b/services/settings/pkg/store/errortypes/errortypes.go index f6c37a644ac..0981f1c5cc0 100644 --- a/services/settings/pkg/store/errortypes/errortypes.go +++ b/services/settings/pkg/store/errortypes/errortypes.go @@ -1,6 +1,8 @@ package errortypes // BundleNotFound is the error to use when a bundle is not found. +// +// Deprecated: use the genreric services/settings/pkg/settings.NotFound error type BundleNotFound string func (e BundleNotFound) Error() string { return "error: bundle not found: " + string(e) } diff --git a/services/settings/pkg/store/filesystem/bundles.go b/services/settings/pkg/store/filesystem/bundles.go index 513bff747f7..f0e5749c84a 100644 --- a/services/settings/pkg/store/filesystem/bundles.go +++ b/services/settings/pkg/store/filesystem/bundles.go @@ -2,6 +2,7 @@ package store import ( + "errors" "fmt" "os" "path/filepath" @@ -9,7 +10,7 @@ import ( "github.com/gofrs/uuid" settingsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/settings/v0" - "github.com/owncloud/ocis/v2/services/settings/pkg/store/errortypes" + "github.com/owncloud/ocis/v2/services/settings/pkg/settings" ) var m = &sync.RWMutex{} @@ -111,7 +112,7 @@ func (s Store) WriteBundle(record *settingsmsg.Bundle) (*settingsmsg.Bundle, err func (s Store) AddSettingToBundle(bundleID string, setting *settingsmsg.Setting) (*settingsmsg.Setting, error) { bundle, err := s.ReadBundle(bundleID) if err != nil { - if _, notFound := err.(errortypes.BundleNotFound); !notFound { + if !errors.Is(err, settings.ErrNotFound) { return nil, err } bundle = new(settingsmsg.Bundle) diff --git a/services/settings/pkg/store/filesystem/io.go b/services/settings/pkg/store/filesystem/io.go index 5a5cccdaa49..a1b0f832d3a 100644 --- a/services/settings/pkg/store/filesystem/io.go +++ b/services/settings/pkg/store/filesystem/io.go @@ -1,10 +1,11 @@ package store import ( + "fmt" "io" "os" - "github.com/owncloud/ocis/v2/services/settings/pkg/store/errortypes" + "github.com/owncloud/ocis/v2/services/settings/pkg/settings" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" ) @@ -13,7 +14,7 @@ import ( func (s Store) parseRecordFromFile(record proto.Message, filePath string) error { _, err := os.Stat(filePath) if err != nil { - return errortypes.BundleNotFound(err.Error()) + return fmt.Errorf("%q: %w", filePath, settings.ErrNotFound) } file, err := os.Open(filePath) @@ -28,7 +29,7 @@ func (s Store) parseRecordFromFile(record proto.Message, filePath string) error } if len(b) == 0 { - return errortypes.BundleNotFound(filePath) + return fmt.Errorf("%q: %w", filePath, settings.ErrNotFound) } if err := protojson.Unmarshal(b, record); err != nil { diff --git a/services/settings/pkg/store/filesystem/permissions.go b/services/settings/pkg/store/filesystem/permissions.go index a01ba654680..1c03e643871 100644 --- a/services/settings/pkg/store/filesystem/permissions.go +++ b/services/settings/pkg/store/filesystem/permissions.go @@ -55,7 +55,7 @@ func (s Store) ReadPermissionByName(name string, roleIDs []string) (*settingsmsg } } } - return nil, settings.ErrPermissionNotFound + return nil, settings.ErrNotFound } // extractPermissionsByResource collects all permissions from the provided role that match the requested resource diff --git a/services/settings/pkg/store/metadata/assignments.go b/services/settings/pkg/store/metadata/assignments.go index 36f2adc44c4..003b248cf33 100644 --- a/services/settings/pkg/store/metadata/assignments.go +++ b/services/settings/pkg/store/metadata/assignments.go @@ -6,8 +6,10 @@ import ( "encoding/json" "fmt" + "github.com/cs3org/reva/v2/pkg/errtypes" "github.com/gofrs/uuid" settingsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/settings/v0" + "github.com/owncloud/ocis/v2/services/settings/pkg/settings" ) // ListRoleAssignments loads and returns all role assignments matching the given assignment identifier. @@ -15,14 +17,24 @@ func (s *Store) ListRoleAssignments(accountUUID string) ([]*settingsmsg.UserRole s.Init() ctx := context.TODO() assIDs, err := s.mdc.ReadDir(ctx, accountPath(accountUUID)) - if err != nil { + switch err.(type) { + case nil: + // continue + case errtypes.NotFound: + return make([]*settingsmsg.UserRoleAssignment, 0), nil + default: return nil, err } ass := make([]*settingsmsg.UserRoleAssignment, 0, len(assIDs)) for _, assID := range assIDs { b, err := s.mdc.SimpleDownload(ctx, assignmentPath(accountUUID, assID)) - if err != nil { + switch err.(type) { + case nil: + // continue + case errtypes.NotFound: + continue + default: return nil, err } @@ -42,10 +54,17 @@ func (s *Store) WriteRoleAssignment(accountUUID, roleID string) (*settingsmsg.Us s.Init() ctx := context.TODO() // as per https://github.com/owncloud/product/issues/103 "Each user can have exactly one role" - _ = s.mdc.Delete(ctx, accountPath(accountUUID)) - // TODO: How to differentiate between 'not found' and other errors? + err := s.mdc.Delete(ctx, accountPath(accountUUID)) + switch err.(type) { + case nil: + // continue + case errtypes.NotFound: + // already gone, continue + default: + return nil, err + } - err := s.mdc.MakeDirIfNotExist(ctx, accountPath(accountUUID)) + err = s.mdc.MakeDirIfNotExist(ctx, accountPath(accountUUID)) if err != nil { return nil, err } @@ -67,7 +86,12 @@ func (s *Store) RemoveRoleAssignment(assignmentID string) error { s.Init() ctx := context.TODO() accounts, err := s.mdc.ReadDir(ctx, accountsFolderLocation) - if err != nil { + switch err.(type) { + case nil: + // continue + case errtypes.NotFound: + return fmt.Errorf("assignmentID '%s' %w", assignmentID, settings.ErrNotFound) + default: return err } @@ -81,11 +105,13 @@ func (s *Store) RemoveRoleAssignment(assignmentID string) error { for _, assID := range assIDs { if assID == assignmentID { - return s.mdc.Delete(ctx, assignmentPath(accID, assID)) + // as per https://github.com/owncloud/product/issues/103 "Each user can have exactly one role" + // we also have to delete the cached dir listing + return s.mdc.Delete(ctx, accountPath(accID)) } } } - return fmt.Errorf("assignmentID '%s' not found", assignmentID) + return fmt.Errorf("assignmentID '%s' %w", assignmentID, settings.ErrNotFound) } func accountPath(accountUUID string) string { diff --git a/services/settings/pkg/store/metadata/bundles.go b/services/settings/pkg/store/metadata/bundles.go index 5e25ac53877..9025c97893c 100644 --- a/services/settings/pkg/store/metadata/bundles.go +++ b/services/settings/pkg/store/metadata/bundles.go @@ -7,8 +7,10 @@ import ( "errors" "fmt" + "github.com/cs3org/reva/v2/pkg/errtypes" "github.com/gofrs/uuid" settingsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/settings/v0" + "github.com/owncloud/ocis/v2/services/settings/pkg/settings" ) // ListBundles returns all bundles in the dataPath folder that match the given type. @@ -18,7 +20,12 @@ func (s *Store) ListBundles(bundleType settingsmsg.Bundle_Type, bundleIDs []stri if len(bundleIDs) == 0 { bIDs, err := s.mdc.ReadDir(ctx, bundleFolderLocation) - if err != nil { + switch err.(type) { + case nil: + // continue + case errtypes.NotFound: + return make([]*settingsmsg.Bundle, 0), nil + default: return nil, err } @@ -27,7 +34,12 @@ func (s *Store) ListBundles(bundleType settingsmsg.Bundle_Type, bundleIDs []stri var bundles []*settingsmsg.Bundle for _, id := range bundleIDs { b, err := s.mdc.SimpleDownload(ctx, bundlePath(id)) - if err != nil { + switch err.(type) { + case nil: + // continue + case errtypes.NotFound: + continue + default: return nil, err } @@ -50,7 +62,12 @@ func (s *Store) ReadBundle(bundleID string) (*settingsmsg.Bundle, error) { s.Init() ctx := context.TODO() b, err := s.mdc.SimpleDownload(ctx, bundlePath(bundleID)) - if err != nil { + switch err.(type) { + case nil: + // continue + case errtypes.NotFound: + return nil, fmt.Errorf("bundleID '%s' %w", bundleID, settings.ErrNotFound) + default: return nil, err } @@ -64,7 +81,12 @@ func (s *Store) ReadSetting(settingID string) (*settingsmsg.Setting, error) { ctx := context.TODO() ids, err := s.mdc.ReadDir(ctx, bundleFolderLocation) - if err != nil { + switch err.(type) { + case nil: + // continue + case errtypes.NotFound: + return nil, fmt.Errorf("settingID '%s' %w", settingID, settings.ErrNotFound) + default: return nil, err } @@ -72,6 +94,9 @@ func (s *Store) ReadSetting(settingID string) (*settingsmsg.Setting, error) { for _, id := range ids { b, err := s.ReadBundle(id) if err != nil { + if errors.Is(err, settings.ErrNotFound) { + continue + } return nil, err } @@ -82,7 +107,7 @@ func (s *Store) ReadSetting(settingID string) (*settingsmsg.Setting, error) { } } - return nil, fmt.Errorf("setting '%s' not found", settingID) + return nil, fmt.Errorf("settingID '%s' %w", settingID, settings.ErrNotFound) } // WriteBundle sends the givens record to the metadataclient. returns `record` for legacy reasons @@ -102,7 +127,9 @@ func (s *Store) AddSettingToBundle(bundleID string, setting *settingsmsg.Setting s.Init() b, err := s.ReadBundle(bundleID) if err != nil { - // TODO: How to differentiate 'not found'? + if !errors.Is(err, settings.ErrNotFound) { + return nil, err + } b = new(settingsmsg.Bundle) b.Id = bundleID b.Type = settingsmsg.Bundle_TYPE_DEFAULT diff --git a/services/settings/pkg/store/metadata/permissions.go b/services/settings/pkg/store/metadata/permissions.go index 9756db8d62f..f13f0bc0992 100644 --- a/services/settings/pkg/store/metadata/permissions.go +++ b/services/settings/pkg/store/metadata/permissions.go @@ -55,7 +55,7 @@ func (s *Store) ReadPermissionByName(name string, roleIDs []string) (*settingsms } } } - return nil, settings.ErrPermissionNotFound + return nil, settings.ErrNotFound } // extractPermissionsByResource collects all permissions from the provided role that match the requested resource diff --git a/services/settings/pkg/store/metadata/store_test.go b/services/settings/pkg/store/metadata/store_test.go index bb1c75ff892..0859d38b524 100644 --- a/services/settings/pkg/store/metadata/store_test.go +++ b/services/settings/pkg/store/metadata/store_test.go @@ -4,6 +4,7 @@ import ( "context" "strings" + "github.com/cs3org/reva/v2/pkg/errtypes" "github.com/owncloud/ocis/v2/services/settings/pkg/config/defaults" ) @@ -53,9 +54,12 @@ func NewMDC(s *Store) error { return s.initMetadataClient(mdc) } -// SimpleDownload returns nil if not found +// SimpleDownload returns errtypes.NotFound if not found func (m *MockedMetadataClient) SimpleDownload(_ context.Context, id string) ([]byte, error) { - return m.data[id], nil + if data, ok := m.data[id]; ok { + return data, nil + } + return nil, errtypes.NotFound("not found") } // SimpleUpload can't error diff --git a/services/settings/pkg/store/metadata/values.go b/services/settings/pkg/store/metadata/values.go index 5ab790f5076..a9df99dacea 100644 --- a/services/settings/pkg/store/metadata/values.go +++ b/services/settings/pkg/store/metadata/values.go @@ -7,8 +7,10 @@ import ( "errors" "fmt" + "github.com/cs3org/reva/v2/pkg/errtypes" "github.com/gofrs/uuid" settingsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/settings/v0" + "github.com/owncloud/ocis/v2/services/settings/pkg/settings" ) // ListValues reads all values that match the given bundleId and accountUUID. @@ -20,7 +22,12 @@ func (s *Store) ListValues(bundleID, accountUUID string) ([]*settingsmsg.Value, ctx := context.TODO() vIDs, err := s.mdc.ReadDir(ctx, valuesFolderLocation) - if err != nil { + switch err.(type) { + case nil: + // continue + case errtypes.NotFound: + return make([]*settingsmsg.Value, 0), nil + default: return nil, err } @@ -28,7 +35,12 @@ func (s *Store) ListValues(bundleID, accountUUID string) ([]*settingsmsg.Value, var values []*settingsmsg.Value for _, vid := range vIDs { b, err := s.mdc.SimpleDownload(ctx, valuePath(vid)) - if err != nil { + switch err.(type) { + case nil: + // continue + case errtypes.NotFound: + continue + default: return nil, err } @@ -61,7 +73,12 @@ func (s *Store) ReadValue(valueID string) (*settingsmsg.Value, error) { ctx := context.TODO() b, err := s.mdc.SimpleDownload(ctx, valuePath(valueID)) - if err != nil { + switch err.(type) { + case nil: + // continue + case errtypes.NotFound: + return nil, fmt.Errorf("valueID '%s' %w", valueID, settings.ErrNotFound) + default: return nil, err } val := &settingsmsg.Value{}