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
parent 1b6c269de5
commit 078698fdf4
29 changed files with 1085 additions and 115 deletions

2
go.mod
View File

@@ -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

4
go.sum
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/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=

View File

@@ -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,

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"`
}

View File

@@ -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:"-"`
}

View File

@@ -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,
},

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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"))
})
})
})

View File

@@ -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
}

View File

@@ -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))
})
})
})

View File

@@ -74,7 +74,7 @@ var _ = Describe("EducationUsers", func() {
service.WithGatewayClient(gatewayClient),
service.EventsPublisher(&eventsPublisher),
service.WithIdentityEducationBackend(identityEducationBackend),
//service.WithRoleService(roleService),
service.WithRoleService(roleService),
)
})

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)
@@ -127,80 +135,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 {
@@ -209,6 +145,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{
@@ -239,6 +181,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)
@@ -252,6 +198,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) {
@@ -322,6 +273,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.

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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) }

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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

View File

@@ -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 {

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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{}