graph: add appRoleAssignments and minimal application resource (#5318)

* bump libregraph-go lib

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>

* add appRoleAssignment stubs

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>

* add get application stub

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>

* fetch appRoles for application from settings service

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>

* initial list appRoleAssignments implementation

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>

* initial create appRoleAssignment implementation, extract assignmentToAppRoleAssignment, configurable app id and displayname

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>

* initial delete appRoleAssignment implementation, changed error handling and logging

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>

* initial expand appRoleAssignment on users

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>

* test user expand appRoleAssignment

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>

* test appRoleAssignment

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>

* fix education test by actually using the mocked roleManager

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>

* test getapplication

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>

* list assignments

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>

* use common not exists error handling

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>

* default to just 'ownCloud Infinite Scale' as application name

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>

* fix store_test

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>

* roll application uuid on init

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>

* fix tests

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>

* extract method

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>

* Apply suggestions from code review

Co-authored-by: Michael Barz <mbarz@owncloud.com>

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
Co-authored-by: Michael Barz <mbarz@owncloud.com>
This commit is contained in:
Jörn Friedrich Dreyer
2023-01-12 16:09:34 +01:00
committed by GitHub
co-authored by Michael Barz
parent 1b6c269de5
commit 078698fdf4
29 changed files with 1085 additions and 115 deletions
+1 -1
View File
@@ -55,7 +55,7 @@ require (
github.com/onsi/ginkgo/v2 v2.7.0 github.com/onsi/ginkgo/v2 v2.7.0
github.com/onsi/gomega v1.24.1 github.com/onsi/gomega v1.24.1
github.com/orcaman/concurrent-map v1.0.0 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/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.14.0 github.com/prometheus/client_golang v1.14.0
github.com/rs/zerolog v1.28.0 github.com/rs/zerolog v1.28.0
+2 -2
View File
@@ -1060,8 +1060,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 h1:I/2A2XPCb4IuQWcQhBhSwGfiuybl/J0ev9HDbW65HOY=
github.com/orcaman/concurrent-map v1.0.0/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI= 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/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.2-0.20230105141655-9384face4d5d h1:aqVf2yJEdSgFQd3k5fnwtYxjTwC/UREAKTZIzeupwHg=
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/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 h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= 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= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
+11 -3
View File
@@ -50,11 +50,15 @@ type LdapBasedService struct {
type Events struct { type Events struct {
TLSInsecure bool `yaml:"tls_insecure"` TLSInsecure bool `yaml:"tls_insecure"`
} }
type GraphApplication struct {
ID string `yaml:"id"`
}
type GraphService struct { type GraphService struct {
Events Events Application GraphApplication
Spaces InsecureService Events Events
Identity LdapBasedService Spaces InsecureService
Identity LdapBasedService
} }
type ServiceUserPasswordsSettings struct { type ServiceUserPasswordsSettings struct {
@@ -219,6 +223,7 @@ func CreateConfig(insecure, forceOverwrite bool, configPath, adminPassword strin
systemUserID := uuid.Must(uuid.NewV4()).String() systemUserID := uuid.Must(uuid.NewV4()).String()
adminUserID := uuid.Must(uuid.NewV4()).String() adminUserID := uuid.Must(uuid.NewV4()).String()
graphApplicationID := uuid.Must(uuid.NewV4()).String()
storageUsersMountID := uuid.Must(uuid.NewV4()).String() storageUsersMountID := uuid.Must(uuid.NewV4()).String()
idmServicePassword, err := generators.GenerateRandomPassword(passwordLength) idmServicePassword, err := generators.GenerateRandomPassword(passwordLength)
@@ -306,6 +311,9 @@ func CreateConfig(insecure, forceOverwrite bool, configPath, adminPassword strin
}, },
}, },
Graph: GraphService{ Graph: GraphService{
Application: GraphApplication{
ID: graphApplicationID,
},
Identity: LdapBasedService{ Identity: LdapBasedService{
Ldap: LdapSettings{ Ldap: LdapSettings{
BindPassword: idmServicePassword, BindPassword: idmServicePassword,
+7
View File
@@ -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"`
}
+4 -3
View File
@@ -25,9 +25,10 @@ type Config struct {
TokenManager *TokenManager `yaml:"token_manager"` TokenManager *TokenManager `yaml:"token_manager"`
GRPCClientTLS *shared.GRPCClientTLS `yaml:"grpc_client_tls"` GRPCClientTLS *shared.GRPCClientTLS `yaml:"grpc_client_tls"`
Spaces Spaces `yaml:"spaces"` Application Application `yaml:"application"`
Identity Identity `yaml:"identity"` Spaces Spaces `yaml:"spaces"`
Events Events `yaml:"events"` Identity Identity `yaml:"identity"`
Events Events `yaml:"events"`
Context context.Context `yaml:"-"` Context context.Context `yaml:"-"`
} }
@@ -31,6 +31,9 @@ func DefaultConfig() *config.Config {
Service: config.Service{ Service: config.Service{
Name: "graph", Name: "graph",
}, },
Application: config.Application{
DisplayName: "ownCloud Infinite Scale",
},
API: config.API{ API: config.API{
GroupMembersPatchLimit: 20, GroupMembersPatchLimit: 20,
}, },
+10
View File
@@ -2,8 +2,10 @@ package parser
import ( import (
"errors" "errors"
"fmt"
ociscfg "github.com/owncloud/ocis/v2/ocis-pkg/config" 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/ocis-pkg/shared"
"github.com/owncloud/ocis/v2/services/graph/pkg/config" "github.com/owncloud/ocis/v2/services/graph/pkg/config"
"github.com/owncloud/ocis/v2/services/graph/pkg/config/defaults" "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) 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 return nil
} }
@@ -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)
}
@@ -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"))
})
})
})
@@ -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
}
@@ -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))
})
})
})
@@ -74,7 +74,7 @@ var _ = Describe("EducationUsers", func() {
service.WithGatewayClient(gatewayClient), service.WithGatewayClient(gatewayClient),
service.EventsPublisher(&eventsPublisher), service.EventsPublisher(&eventsPublisher),
service.WithIdentityEducationBackend(identityEducationBackend), service.WithIdentityEducationBackend(identityEducationBackend),
//service.WithRoleService(roleService), service.WithRoleService(roleService),
) )
}) })
@@ -24,6 +24,16 @@ func (i instrument) ServeHTTP(w http.ResponseWriter, r *http.Request) {
i.next.ServeHTTP(w, r) 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. // GetMe implements the Service interface.
func (i instrument) GetMe(w http.ResponseWriter, r *http.Request) { func (i instrument) GetMe(w http.ResponseWriter, r *http.Request) {
i.next.GetMe(w, r) i.next.GetMe(w, r)
@@ -59,6 +69,21 @@ func (i instrument) ChangeOwnPassword(w http.ResponseWriter, r *http.Request) {
i.next.ChangeOwnPassword(w, r) 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. // GetGroups implements the Service interface.
func (i instrument) GetGroups(w http.ResponseWriter, r *http.Request) { func (i instrument) GetGroups(w http.ResponseWriter, r *http.Request) {
i.next.GetGroups(w, r) i.next.GetGroups(w, r)
+25
View File
@@ -24,6 +24,16 @@ func (l logging) ServeHTTP(w http.ResponseWriter, r *http.Request) {
l.next.ServeHTTP(w, r) 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. // GetMe implements the Service interface.
func (l logging) GetMe(w http.ResponseWriter, r *http.Request) { func (l logging) GetMe(w http.ResponseWriter, r *http.Request) {
l.next.GetMe(w, r) l.next.GetMe(w, r)
@@ -59,6 +69,21 @@ func (l logging) ChangeOwnPassword(w http.ResponseWriter, r *http.Request) {
l.next.ChangeOwnPassword(w, r) 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. // GetGroups implements the Service interface.
func (l logging) GetGroups(w http.ResponseWriter, r *http.Request) { func (l logging) GetGroups(w http.ResponseWriter, r *http.Request) {
l.next.GetGroups(w, r) l.next.GetGroups(w, r)
+107 -74
View File
@@ -32,6 +32,10 @@ const (
// Service defines the service handlers. // Service defines the service handlers.
type Service interface { type Service interface {
ServeHTTP(http.ResponseWriter, *http.Request) ServeHTTP(http.ResponseWriter, *http.Request)
ListApplications(w http.ResponseWriter, r *http.Request)
GetApplication(http.ResponseWriter, *http.Request)
GetMe(http.ResponseWriter, *http.Request) GetMe(http.ResponseWriter, *http.Request)
GetUsers(http.ResponseWriter, *http.Request) GetUsers(http.ResponseWriter, *http.Request)
GetUser(http.ResponseWriter, *http.Request) GetUser(http.ResponseWriter, *http.Request)
@@ -40,6 +44,10 @@ type Service interface {
PatchUser(http.ResponseWriter, *http.Request) PatchUser(http.ResponseWriter, *http.Request)
ChangeOwnPassword(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) GetGroups(http.ResponseWriter, *http.Request)
GetGroup(http.ResponseWriter, *http.Request) GetGroup(http.ResponseWriter, *http.Request)
PostGroup(http.ResponseWriter, *http.Request) PostGroup(http.ResponseWriter, *http.Request)
@@ -127,80 +135,8 @@ func NewService(opts ...Option) (Graph, error) {
identityEducationBackend: options.IdentityEducationBackend, identityEducationBackend: options.IdentityEducationBackend,
} }
if options.IdentityBackend == nil { if err := setIdentityBackends(options, &svc); err != nil {
switch options.Config.Identity.Backend { return svc, err
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 options.PermissionService == nil { if options.PermissionService == nil {
@@ -209,6 +145,12 @@ func NewService(opts ...Option) (Graph, error) {
svc.permissionsService = options.PermissionService 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 roleManager := options.RoleManager
if roleManager == nil { if roleManager == nil {
storeOptions := store.OcisStoreOptions{ storeOptions := store.OcisStoreOptions{
@@ -239,6 +181,10 @@ func NewService(opts ...Option) (Graph, error) {
r.Put("/tags", svc.AssignTags) r.Put("/tags", svc.AssignTags)
r.Delete("/tags", svc.UnassignTags) 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.Route("/me", func(r chi.Router) {
r.Get("/", svc.GetMe) r.Get("/", svc.GetMe)
r.Get("/drives", svc.GetDrives) r.Get("/drives", svc.GetDrives)
@@ -252,6 +198,11 @@ func NewService(opts ...Option) (Graph, error) {
r.Get("/", svc.GetUser) r.Get("/", svc.GetUser)
r.With(requireAdmin).Delete("/", svc.DeleteUser) r.With(requireAdmin).Delete("/", svc.DeleteUser)
r.With(requireAdmin).Patch("/", svc.PatchUser) 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) { r.Route("/groups", func(r chi.Router) {
@@ -322,6 +273,88 @@ func NewService(opts ...Option) (Graph, error) {
return svc, nil 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. // parseHeaderPurge parses the 'Purge' header.
// '1', 't', 'T', 'TRUE', 'true', 'True' are parsed as true // '1', 't', 'T', 'TRUE', 'true', 'True' are parsed as true
// all other values are false. // all other values are false.
+25
View File
@@ -20,6 +20,16 @@ func (t tracing) ServeHTTP(w http.ResponseWriter, r *http.Request) {
t.next.ServeHTTP(w, r) 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. // GetMe implements the Service interface.
func (t tracing) GetMe(w http.ResponseWriter, r *http.Request) { func (t tracing) GetMe(w http.ResponseWriter, r *http.Request) {
t.next.GetMe(w, r) t.next.GetMe(w, r)
@@ -55,6 +65,21 @@ func (t tracing) ChangeOwnPassword(w http.ResponseWriter, r *http.Request) {
t.next.ChangeOwnPassword(w, r) 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. // GetGroups implements the Service interface.
func (t tracing) GetGroups(w http.ResponseWriter, r *http.Request) { func (t tracing) GetGroups(w http.ResponseWriter, r *http.Request) {
t.next.GetGroups(w, r) t.next.GetGroups(w, r)
+67
View File
@@ -58,6 +58,30 @@ func (g Graph) GetMe(w http.ResponseWriter, r *http.Request) {
return 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.Status(r, http.StatusOK)
render.JSON(w, r, me) render.JSON(w, r, me)
} }
@@ -87,6 +111,26 @@ func (g Graph) GetUsers(w http.ResponseWriter, r *http.Request) {
return 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) users, err = sortUsers(odataReq, users)
if err != nil { if err != nil {
logger.Debug().Interface("query", odataReq).Msg("error while sorting users according to query") 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.Status(r, http.StatusOK)
render.JSON(w, r, user) render.JSON(w, r, user)
+148 -4
View File
@@ -17,11 +17,13 @@ import (
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"go-micro.dev/v4/client"
libregraph "github.com/owncloud/libre-graph-api-go" libregraph "github.com/owncloud/libre-graph-api-go"
ogrpc "github.com/owncloud/ocis/v2/ocis-pkg/service/grpc" ogrpc "github.com/owncloud/ocis/v2/ocis-pkg/service/grpc"
"github.com/owncloud/ocis/v2/ocis-pkg/shared" "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/mocks"
"github.com/owncloud/ocis/v2/services/graph/pkg/config" "github.com/owncloud/ocis/v2/services/graph/pkg/config"
"github.com/owncloud/ocis/v2/services/graph/pkg/config/defaults" "github.com/owncloud/ocis/v2/services/graph/pkg/config/defaults"
@@ -67,6 +69,7 @@ var _ = Describe("Users", func() {
cfg.TokenManager.JWTSecret = "loremipsum" cfg.TokenManager.JWTSecret = "loremipsum"
cfg.Commons = &shared.Commons{} cfg.Commons = &shared.Commons{}
cfg.GRPCClientTLS = &shared.GRPCClientTLS{} cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
cfg.Application.ID = "some-application-ID"
_ = ogrpc.Configure(ogrpc.GetClientOptions(cfg.GRPCClientTLS)...) _ = ogrpc.Configure(ogrpc.GetClientOptions(cfg.GRPCClientTLS)...)
svc, _ = service.NewService( svc, _ = service.NewService(
@@ -94,8 +97,13 @@ var _ = Describe("Users", func() {
Expect(rr.Code).To(Equal(http.StatusOK)) Expect(rr.Code).To(Equal(http.StatusOK))
}) })
It("expands the user", func() { It("expands the memberOf", func() {
user := &libregraph.User{} 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) identityBackend.On("GetUser", mock.Anything, mock.Anything, mock.Anything).Return(user, nil)
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me?$expand=memberOf", 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) svc.GetMe(rr, r)
Expect(rr.Code).To(Equal(http.StatusOK)) 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)) 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() { Describe("GetUser", func() {
@@ -322,6 +428,44 @@ var _ = Describe("Users", func() {
Expect(responseUser.GetId()).To(Equal("user1")) Expect(responseUser.GetId()).To(Equal("user1"))
Expect(len(responseUser.GetDrives())).To(Equal(1)) 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() { Describe("PostUser", func() {
@@ -381,7 +525,7 @@ var _ = Describe("Users", func() {
}) })
It("creates a user", 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 { identityBackend.On("CreateUser", mock.Anything, mock.Anything).Return(func(ctx context.Context, user libregraph.User) *libregraph.User {
user.SetId("/users/user") user.SetId("/users/user")
return &user return &user
+1 -1
View File
@@ -79,7 +79,7 @@ func (g Service) CheckPermission(ctx context.Context, req *permissions.CheckPerm
permission, err := g.manager.ReadPermissionByName(req.Permission, roleIDs) permission, err := g.manager.ReadPermissionByName(req.Permission, roleIDs)
if err != nil { if err != nil {
if !errors.Is(err, settings.ErrPermissionNotFound) { if !errors.Is(err, settings.ErrNotFound) {
return &permissions.CheckPermissionResponse{ return &permissions.CheckPermissionResponse{
Status: status.NewInternal(ctx, err.Error()), Status: status.NewInternal(ctx, err.Error()),
}, nil }, nil
@@ -12,7 +12,12 @@ var (
Registry = map[string]RegisterFunc{} Registry = map[string]RegisterFunc{}
// ErrPermissionNotFound defines a new error for when a permission was not found // ErrPermissionNotFound defines a new error for when a permission was not found
//
// Deprecated use the more generic ErrNotFound
ErrPermissionNotFound = errors.New("permission not found") 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 // RegisterFunc stores store constructors
@@ -1,6 +1,8 @@
package errortypes package errortypes
// BundleNotFound is the error to use when a bundle is not found. // 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 type BundleNotFound string
func (e BundleNotFound) Error() string { return "error: bundle not found: " + string(e) } func (e BundleNotFound) Error() string { return "error: bundle not found: " + string(e) }
@@ -2,6 +2,7 @@
package store package store
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
@@ -9,7 +10,7 @@ import (
"github.com/gofrs/uuid" "github.com/gofrs/uuid"
settingsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/settings/v0" 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{} 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) { func (s Store) AddSettingToBundle(bundleID string, setting *settingsmsg.Setting) (*settingsmsg.Setting, error) {
bundle, err := s.ReadBundle(bundleID) bundle, err := s.ReadBundle(bundleID)
if err != nil { if err != nil {
if _, notFound := err.(errortypes.BundleNotFound); !notFound { if !errors.Is(err, settings.ErrNotFound) {
return nil, err return nil, err
} }
bundle = new(settingsmsg.Bundle) bundle = new(settingsmsg.Bundle)
+4 -3
View File
@@ -1,10 +1,11 @@
package store package store
import ( import (
"fmt"
"io" "io"
"os" "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/encoding/protojson"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )
@@ -13,7 +14,7 @@ import (
func (s Store) parseRecordFromFile(record proto.Message, filePath string) error { func (s Store) parseRecordFromFile(record proto.Message, filePath string) error {
_, err := os.Stat(filePath) _, err := os.Stat(filePath)
if err != nil { if err != nil {
return errortypes.BundleNotFound(err.Error()) return fmt.Errorf("%q: %w", filePath, settings.ErrNotFound)
} }
file, err := os.Open(filePath) file, err := os.Open(filePath)
@@ -28,7 +29,7 @@ func (s Store) parseRecordFromFile(record proto.Message, filePath string) error
} }
if len(b) == 0 { if len(b) == 0 {
return errortypes.BundleNotFound(filePath) return fmt.Errorf("%q: %w", filePath, settings.ErrNotFound)
} }
if err := protojson.Unmarshal(b, record); err != nil { if err := protojson.Unmarshal(b, record); err != nil {
@@ -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 // extractPermissionsByResource collects all permissions from the provided role that match the requested resource
@@ -6,8 +6,10 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/gofrs/uuid" "github.com/gofrs/uuid"
settingsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/settings/v0" 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. // 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() s.Init()
ctx := context.TODO() ctx := context.TODO()
assIDs, err := s.mdc.ReadDir(ctx, accountPath(accountUUID)) 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 return nil, err
} }
ass := make([]*settingsmsg.UserRoleAssignment, 0, len(assIDs)) ass := make([]*settingsmsg.UserRoleAssignment, 0, len(assIDs))
for _, assID := range assIDs { for _, assID := range assIDs {
b, err := s.mdc.SimpleDownload(ctx, assignmentPath(accountUUID, assID)) 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 return nil, err
} }
@@ -42,10 +54,17 @@ func (s *Store) WriteRoleAssignment(accountUUID, roleID string) (*settingsmsg.Us
s.Init() s.Init()
ctx := context.TODO() ctx := context.TODO()
// as per https://github.com/owncloud/product/issues/103 "Each user can have exactly one role" // as per https://github.com/owncloud/product/issues/103 "Each user can have exactly one role"
_ = s.mdc.Delete(ctx, accountPath(accountUUID)) err := s.mdc.Delete(ctx, accountPath(accountUUID))
// TODO: How to differentiate between 'not found' and other errors? 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 { if err != nil {
return nil, err return nil, err
} }
@@ -67,7 +86,12 @@ func (s *Store) RemoveRoleAssignment(assignmentID string) error {
s.Init() s.Init()
ctx := context.TODO() ctx := context.TODO()
accounts, err := s.mdc.ReadDir(ctx, accountsFolderLocation) 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 return err
} }
@@ -81,11 +105,13 @@ func (s *Store) RemoveRoleAssignment(assignmentID string) error {
for _, assID := range assIDs { for _, assID := range assIDs {
if assID == assignmentID { 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 { func accountPath(accountUUID string) string {
@@ -7,8 +7,10 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/gofrs/uuid" "github.com/gofrs/uuid"
settingsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/settings/v0" 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. // 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 { if len(bundleIDs) == 0 {
bIDs, err := s.mdc.ReadDir(ctx, bundleFolderLocation) 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 return nil, err
} }
@@ -27,7 +34,12 @@ func (s *Store) ListBundles(bundleType settingsmsg.Bundle_Type, bundleIDs []stri
var bundles []*settingsmsg.Bundle var bundles []*settingsmsg.Bundle
for _, id := range bundleIDs { for _, id := range bundleIDs {
b, err := s.mdc.SimpleDownload(ctx, bundlePath(id)) 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 return nil, err
} }
@@ -50,7 +62,12 @@ func (s *Store) ReadBundle(bundleID string) (*settingsmsg.Bundle, error) {
s.Init() s.Init()
ctx := context.TODO() ctx := context.TODO()
b, err := s.mdc.SimpleDownload(ctx, bundlePath(bundleID)) 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 return nil, err
} }
@@ -64,7 +81,12 @@ func (s *Store) ReadSetting(settingID string) (*settingsmsg.Setting, error) {
ctx := context.TODO() ctx := context.TODO()
ids, err := s.mdc.ReadDir(ctx, bundleFolderLocation) 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 return nil, err
} }
@@ -72,6 +94,9 @@ func (s *Store) ReadSetting(settingID string) (*settingsmsg.Setting, error) {
for _, id := range ids { for _, id := range ids {
b, err := s.ReadBundle(id) b, err := s.ReadBundle(id)
if err != nil { if err != nil {
if errors.Is(err, settings.ErrNotFound) {
continue
}
return nil, err 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 // 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() s.Init()
b, err := s.ReadBundle(bundleID) b, err := s.ReadBundle(bundleID)
if err != nil { if err != nil {
// TODO: How to differentiate 'not found'? if !errors.Is(err, settings.ErrNotFound) {
return nil, err
}
b = new(settingsmsg.Bundle) b = new(settingsmsg.Bundle)
b.Id = bundleID b.Id = bundleID
b.Type = settingsmsg.Bundle_TYPE_DEFAULT b.Type = settingsmsg.Bundle_TYPE_DEFAULT
@@ -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 // extractPermissionsByResource collects all permissions from the provided role that match the requested resource
@@ -4,6 +4,7 @@ import (
"context" "context"
"strings" "strings"
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/owncloud/ocis/v2/services/settings/pkg/config/defaults" "github.com/owncloud/ocis/v2/services/settings/pkg/config/defaults"
) )
@@ -53,9 +54,12 @@ func NewMDC(s *Store) error {
return s.initMetadataClient(mdc) 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) { 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 // SimpleUpload can't error
+20 -3
View File
@@ -7,8 +7,10 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/gofrs/uuid" "github.com/gofrs/uuid"
settingsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/settings/v0" 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. // 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() ctx := context.TODO()
vIDs, err := s.mdc.ReadDir(ctx, valuesFolderLocation) 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 return nil, err
} }
@@ -28,7 +35,12 @@ func (s *Store) ListValues(bundleID, accountUUID string) ([]*settingsmsg.Value,
var values []*settingsmsg.Value var values []*settingsmsg.Value
for _, vid := range vIDs { for _, vid := range vIDs {
b, err := s.mdc.SimpleDownload(ctx, valuePath(vid)) 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 return nil, err
} }
@@ -61,7 +73,12 @@ func (s *Store) ReadValue(valueID string) (*settingsmsg.Value, error) {
ctx := context.TODO() ctx := context.TODO()
b, err := s.mdc.SimpleDownload(ctx, valuePath(valueID)) 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 return nil, err
} }
val := &settingsmsg.Value{} val := &settingsmsg.Value{}