Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

graph: add appRoleAssignments and minimal application resource #5318

Merged
merged 20 commits into from
Jan 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • bump to a release

github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.14.0
github.com/rs/zerolog v1.28.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1058,8 +1058,8 @@ github.com/oracle/oci-go-sdk v24.3.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35uk
github.com/orcaman/concurrent-map v1.0.0 h1:I/2A2XPCb4IuQWcQhBhSwGfiuybl/J0ev9HDbW65HOY=
github.com/orcaman/concurrent-map v1.0.0/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
github.com/ovh/go-ovh v1.1.0/go.mod h1:AxitLZ5HBRPyUd+Zl60Ajaag+rNTdVXWIkzfrVuTXWA=
github.com/owncloud/libre-graph-api-go v1.0.1 h1:wj3aQQr/yDPoc97ddg7DCadvMx6ui6N7re/oRV9+yNs=
github.com/owncloud/libre-graph-api-go v1.0.1/go.mod h1:579sFrPP7aP24LZXGPopLfvE+hAka/2DYHk0+Ij+w+U=
github.com/owncloud/libre-graph-api-go v1.0.2-0.20230105141655-9384face4d5d h1:aqVf2yJEdSgFQd3k5fnwtYxjTwC/UREAKTZIzeupwHg=
github.com/owncloud/libre-graph-api-go v1.0.2-0.20230105141655-9384face4d5d/go.mod h1:iKdVH6nYpI8RBeK9sjeLfzrPByST6r9d+NG2IJHoJmU=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
Expand Down
14 changes: 11 additions & 3 deletions ocis/pkg/init/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -306,6 +311,9 @@ func CreateConfig(insecure, forceOverwrite bool, configPath, adminPassword strin
},
},
Graph: GraphService{
Application: GraphApplication{
ID: graphApplicationID,
},
Identity: LdapBasedService{
Ldap: LdapSettings{
BindPassword: idmServicePassword,
Expand Down
7 changes: 7 additions & 0 deletions services/graph/pkg/config/application.go
Original file line number Diff line number Diff line change
@@ -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."`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@butonic could you add a changelog item here? Just pulled the latest oCIS docker image and had it fail with an error until I added the GRAPH_APPLICATION_ID ;)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AKA "I went to the changelog first, didn't find anything obvious and ended up here in your PR"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry, yes ... I hope you got at least a meaningful error message?

Unfortunately, rolling a random application id also is also suboptimal because we would have to write it to the yaml config, which might be readonly ... I'm thinking about a solution for that. maybe store it in the metadata instead of the yaml file ...

DisplayName string `yaml:"displayname" env:"GRAPH_APPLICATION_DISPLAYNAME" desc:"The oCIS application name"`
}
7 changes: 4 additions & 3 deletions services/graph/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:"-"`
}
Expand Down
3 changes: 3 additions & 0 deletions services/graph/pkg/config/defaults/defaultconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
10 changes: 10 additions & 0 deletions services/graph/pkg/config/parser/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
77 changes: 77 additions & 0 deletions services/graph/pkg/service/v0/application.go
Original file line number Diff line number Diff line change
@@ -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)
}
135 changes: 135 additions & 0 deletions services/graph/pkg/service/v0/application_test.go
Original file line number Diff line number Diff line change
@@ -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"))
})
})

})
Loading