diff --git a/changelog/unreleased/single-space-enpoint.md b/changelog/unreleased/single-space-enpoint.md new file mode 100644 index 0000000000..c77e6a461a --- /dev/null +++ b/changelog/unreleased/single-space-enpoint.md @@ -0,0 +1,5 @@ +Enhancement: Add endpoint to retrieve a single space + +We added the endpoint ``/drives/{driveID}`` to get a single space by id from the server. + +https://github.com/owncloud/ocis/pull/2978 diff --git a/graph/pkg/service/v0/drives.go b/graph/pkg/service/v0/drives.go index 0d15adc48d..2757c8cd98 100644 --- a/graph/pkg/service/v0/drives.go +++ b/graph/pkg/service/v0/drives.go @@ -43,46 +43,20 @@ func (g Graph) GetDrives(w http.ResponseWriter, r *http.Request) { errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error()) return } - g.logger.Info().Msg("Calling GetDrives") + g.logger.Info().Interface("query", r.URL.Query()).Msg("Calling GetDrives") ctx := r.Context() - client := g.GetGatewayClient() - - permissions := make(map[string]struct{}, 1) - s := sproto.NewPermissionService("com.owncloud.api.settings", grpc.DefaultClient) - - _, err = s.GetPermissionByID(ctx, &sproto.GetPermissionByIDRequest{ - PermissionId: settingsSvc.ListAllSpacesPermissionID, - }) - - // No error means the user has the permission - if err == nil { - permissions[settingsSvc.ListAllSpacesPermissionName] = struct{}{} - } - value, err := json.Marshal(permissions) - if err != nil { - errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) - return - } filters, err := generateCs3Filters(odataReq) if err != nil { g.logger.Err(err).Interface("query", r.URL.Query()).Msg("query error") errorcode.NotSupported.Render(w, r, http.StatusNotImplemented, err.Error()) return } - res, err := client.ListStorageSpaces(ctx, &storageprovider.ListStorageSpacesRequest{ - Opaque: &types.Opaque{Map: map[string]*types.OpaqueEntry{ - "permissions": { - Decoder: "json", - Value: value, - }, - }}, - Filters: filters, - }) + res, err := g.ListStorageSpacesWithFilters(ctx, filters) switch { case err != nil: - g.logger.Error().Err(err).Msg("error sending list storage spaces grpc request") - errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error()) + g.logger.Error().Err(err).Msg(ListStorageSpacesTransportErr) + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) return case res.Status.Code != cs3rpc.Code_CODE_OK: if res.Status.Code == cs3rpc.Code_CODE_NOT_FOUND { @@ -91,7 +65,7 @@ func (g Graph) GetDrives(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, &listResponse{}) return } - g.logger.Error().Err(err).Msg("error sending list storage spaces grpc request") + g.logger.Error().Err(err).Msg(ListStorageSpacesReturnsErr) errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, res.Status.Message) return } @@ -113,6 +87,75 @@ func (g Graph) GetDrives(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, &listResponse{Value: files}) } +// GetSingleDrive does a lookup of a single space by spaceId +func (g Graph) GetSingleDrive(w http.ResponseWriter, r *http.Request) { + driveID := chi.URLParam(r, "driveID") + if driveID == "" { + err := fmt.Errorf("no valid space id retrieved") + g.logger.Err(err) + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) + return + } + + g.logger.Info().Str("driveID", driveID).Msg("Calling GetSingleDrive") + ctx := r.Context() + + filters := []*storageprovider.ListStorageSpacesRequest_Filter{ + { + Type: storageprovider.ListStorageSpacesRequest_Filter_TYPE_ID, + Term: &storageprovider.ListStorageSpacesRequest_Filter_Id{ + Id: &storageprovider.StorageSpaceId{ + OpaqueId: driveID, + }, + }, + }, + } + res, err := g.ListStorageSpacesWithFilters(ctx, filters) + switch { + case err != nil: + g.logger.Error().Err(err).Msg(ListStorageSpacesTransportErr) + errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error()) + return + case res.Status.Code != cs3rpc.Code_CODE_OK: + if res.Status.Code == cs3rpc.Code_CODE_NOT_FOUND { + // the client is doing a lookup for a specific space, therefore we need to return + // not found to the caller + g.logger.Error().Str("driveID", driveID).Msg(fmt.Sprintf(NoSpaceFoundMessage, driveID)) + errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, fmt.Sprintf(NoSpaceFoundMessage, driveID)) + return + } + g.logger.Error().Err(err).Msg(ListStorageSpacesReturnsErr) + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, res.Status.Message) + return + } + + wdu, err := url.Parse(g.config.Spaces.WebDavBase + g.config.Spaces.WebDavPath) + if err != nil { + g.logger.Error().Err(err).Msg("error parsing url") + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) + return + } + spaces, err := g.formatDrives(ctx, wdu, res.StorageSpaces) + if err != nil { + g.logger.Error().Err(err).Msg("error encoding response") + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) + return + } + switch num := len(spaces); { + case num == 0: + g.logger.Error().Str("driveID", driveID).Msg("no space found") + errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, fmt.Sprintf(NoSpaceFoundMessage, driveID)) + return + case num == 1: + render.Status(r, http.StatusOK) + render.JSON(w, r, spaces[0]) + default: + g.logger.Error().Int("number", num).Msg("expected to find a single space but found more") + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "expected to find a single space but found more") + return + } +} + // CreateDrive creates a storage drive (space). func (g Graph) CreateDrive(w http.ResponseWriter, r *http.Request) { us, ok := ctxpkg.ContextGetUser(r.Context()) @@ -340,6 +383,38 @@ func (g Graph) formatDrives(ctx context.Context, baseURL *url.URL, mds []*storag return responses, nil } +// ListStorageSpacesWithFilters List Storage Spaces using filters +func (g Graph) ListStorageSpacesWithFilters(ctx context.Context, filters []*storageprovider.ListStorageSpacesRequest_Filter) (*storageprovider.ListStorageSpacesResponse, error) { + client := g.GetGatewayClient() + + permissions := make(map[string]struct{}, 1) + s := sproto.NewPermissionService("com.owncloud.api.settings", grpc.DefaultClient) + + _, err := s.GetPermissionByID(ctx, &sproto.GetPermissionByIDRequest{ + PermissionId: settingsSvc.ListAllSpacesPermissionID, + }) + + // No error means the user has the permission + if err == nil { + permissions[settingsSvc.ListAllSpacesPermissionName] = struct{}{} + } + value, err := json.Marshal(permissions) + if err != nil { + return nil, err + } + + res, err := client.ListStorageSpaces(ctx, &storageprovider.ListStorageSpacesRequest{ + Opaque: &types.Opaque{Map: map[string]*types.OpaqueEntry{ + "permissions": { + Decoder: "json", + Value: value, + }, + }}, + Filters: filters, + }) + return res, err +} + func cs3StorageSpaceToDrive(baseURL *url.URL, space *storageprovider.StorageSpace) (*libregraph.Drive, error) { rootID := space.Root.StorageId + "!" + space.Root.OpaqueId if space.Root.StorageId == space.Root.OpaqueId { diff --git a/graph/pkg/service/v0/graph.go b/graph/pkg/service/v0/graph.go index 3523dd8fb5..8117859194 100644 --- a/graph/pkg/service/v0/graph.go +++ b/graph/pkg/service/v0/graph.go @@ -85,3 +85,9 @@ func (g Graph) GetHTTPClient() HTTPClient { type listResponse struct { Value interface{} `json:"value,omitempty"` } + +const ( + NoSpaceFoundMessage = "space with id `%s` not found" + ListStorageSpacesTransportErr = "transport error sending list storage spaces grpc request" + ListStorageSpacesReturnsErr = "list storage spaces grpc request returns an errorcode in the response" +) diff --git a/graph/pkg/service/v0/service.go b/graph/pkg/service/v0/service.go index 5d62e3cdb7..529fec816b 100644 --- a/graph/pkg/service/v0/service.go +++ b/graph/pkg/service/v0/service.go @@ -117,6 +117,7 @@ func NewService(opts ...Option) Service { r.Post("/", svc.CreateDrive) r.Route("/{driveID}", func(r chi.Router) { r.Patch("/", svc.UpdateDrive) + r.Get("/", svc.GetSingleDrive) }) }) }) diff --git a/tests/acceptance/features/apiSpaces/listSpaces.feature b/tests/acceptance/features/apiSpaces/listSpaces.feature index 175c562cc7..ac3afc65e9 100644 --- a/tests/acceptance/features/apiSpaces/listSpaces.feature +++ b/tests/acceptance/features/apiSpaces/listSpaces.feature @@ -79,3 +79,35 @@ Feature: List and create spaces | name | Project Venus | | quota@@@total | 2000 | | root@@@webDavUrl | %base_url%/dav/spaces/%space_id% | + + Scenario: A user can list his personal space via multiple endpoints + When user "Alice" lists all available spaces via the GraphApi with query "$filter=driveType eq 'personal'" + Then the json responded should contain a space "Alice Hansen" with these key and value pairs: + | key | value | + | driveType | personal | + | name | Alice Hansen | + | root@@@webDavUrl | %base_url%/dav/spaces/%space_id% | + When user "Alice" looks up the single space "Alice Hansen" via the GraphApi by using its id + Then the json responded should contain a space "Alice Hansen" with these key and value pairs: + | key | value | + | driveType | personal | + | name | Alice Hansen | + | root@@@webDavUrl | %base_url%/dav/spaces/%space_id% | + + Scenario: A user can list his created spaces via multiple endpoints + Given the administrator has given "Alice" the role "Admin" using the settings api + When user "Alice" creates a space "Project Venus" of type "project" with quota "2000" using the GraphApi + Then the HTTP status code should be "201" + And the json responded should contain a space "Project Venus" with these key and value pairs: + | key | value | + | driveType | project | + | name | Project Venus | + | quota@@@total | 2000 | + | root@@@webDavUrl | %base_url%/dav/spaces/%space_id% | + When user "Alice" looks up the single space "Project Venus" via the GraphApi by using its id + Then the json responded should contain a space "Project Venus" with these key and value pairs: + | key | value | + | driveType | project | + | name | Project Venus | + | quota@@@total | 2000 | + | root@@@webDavUrl | %base_url%/dav/spaces/%space_id% | diff --git a/tests/acceptance/features/bootstrap/SpacesContext.php b/tests/acceptance/features/bootstrap/SpacesContext.php index c5d5a220ae..b0e03b2ef8 100644 --- a/tests/acceptance/features/bootstrap/SpacesContext.php +++ b/tests/acceptance/features/bootstrap/SpacesContext.php @@ -232,7 +232,7 @@ class SpacesContext implements Context { } /** - * Send Graph List Spaces Request + * Send Graph List My Spaces Request * * @param string $user * @param string $password @@ -245,7 +245,7 @@ class SpacesContext implements Context { * * @throws GuzzleException */ - public function listSpacesRequest( + public function listMySpacesRequest( string $user, string $password, string $urlArguments = '', @@ -258,6 +258,34 @@ class SpacesContext implements Context { return HttpRequestHelper::get($fullUrl, $xRequestId, $user, $password, $headers, $body); } + /** + * Send Graph List Single Space Request + * + * @param string $user + * @param string $password + * @param string $urlArguments + * @param string $xRequestId + * @param array $body + * @param array $headers + * + * @return ResponseInterface + * + * @throws GuzzleException + */ + public function listSingleSpaceRequest( + string $user, + string $password, + string $spaceId, + string $urlArguments = '', + string $xRequestId = '', + array $body = [], + array $headers = [] + ): ResponseInterface { + $fullUrl = $this->baseUrl . "/graph/v1.0/drives/" . $spaceId . "/" . $urlArguments; + + return HttpRequestHelper::get($fullUrl, $xRequestId, $user, $password, $headers, $body); + } + /** * Send Graph Create Space Request * @@ -342,7 +370,7 @@ class SpacesContext implements Context { */ public function theUserListsAllHisAvailableSpacesUsingTheGraphApi(string $user): void { $this->featureContext->setResponse( - $this->listSpacesRequest( + $this->listMySpacesRequest( $user, $this->featureContext->getPasswordForUser($user) ) @@ -362,7 +390,7 @@ class SpacesContext implements Context { */ public function theUserListsAllHisAvailableSpacesUsingTheGraphApiWithFilter(string $user, string $query): void { $this->featureContext->setResponse( - $this->listSpacesRequest( + $this->listMySpacesRequest( $user, $this->featureContext->getPasswordForUser($user), "?". $query @@ -370,6 +398,30 @@ class SpacesContext implements Context { ); } + /** + * @When /^user "([^"]*)" looks up the single space "([^"]*)" via the GraphApi by using its id$/ + * + * @param string $user + * @param string $query + * + * @return void + * + * @throws GuzzleException + */ + public function theUserLooksUpTheSingleSpaceUsingTheGraphApiByUsingItsId(string $user, string $spaceName): void { + $space = $this->getSpaceByName($user, $spaceName); + Assert::assertIsArray($space); + Assert::assertNotEmpty($spaceId = $space["id"]); + Assert::assertNotEmpty($spaceWebDavUrl = $space["root"]["webDavUrl"]); + $this->featureContext->setResponse( + $this->listSingleSpaceRequest( + $user, + $this->featureContext->getPasswordForUser($user), + $spaceId + ) + ); + } + /** * @When /^user "([^"]*)" creates a space "([^"]*)" of type "([^"]*)" with the default quota using the GraphApi$/ *