diff --git a/changelog/unreleased/implement-ocm-wellknown.md b/changelog/unreleased/implement-ocm-wellknown.md new file mode 100644 index 0000000000..7a4796c549 --- /dev/null +++ b/changelog/unreleased/implement-ocm-wellknown.md @@ -0,0 +1,5 @@ +Enhancement: Implement OCM well-known endpoint + +The `wellknown` service now implements the `/.well-known/ocm` endpoint for OCM discovery. The unused endpoints for openid connect and webfinger have been removed. This aligns the wellknown implementation with the master branch. + +https://github.com/cs3org/reva/pull/4809 diff --git a/internal/http/services/wellknown/ocm.go b/internal/http/services/wellknown/ocm.go new file mode 100644 index 0000000000..4644cefe04 --- /dev/null +++ b/internal/http/services/wellknown/ocm.go @@ -0,0 +1,148 @@ +// Copyright 2018-2024 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package wellknown + +import ( + "encoding/json" + "net/http" + "net/url" + "path/filepath" + + "github.com/cs3org/reva/v2/pkg/appctx" +) + +const OCMAPIVersion = "1.1.0" + +type OcmProviderConfig struct { + OCMPrefix string `docs:"ocm;The prefix URL where the OCM API is served." mapstructure:"ocm_prefix"` + Endpoint string `docs:"This host's full URL. If it's not configured, it is assumed OCM is not available." mapstructure:"endpoint"` + Provider string `docs:"reva;A friendly name that defines this service." mapstructure:"provider"` + WebdavRoot string `docs:"/remote.php/dav/ocm;The root URL of the WebDAV endpoint to serve OCM shares." mapstructure:"webdav_root"` + WebappRoot string `docs:"/external/sciencemesh;The root URL to serve Web apps via OCM." mapstructure:"webapp_root"` + EnableWebapp bool `docs:"false;Whether web apps are enabled in OCM shares." mapstructure:"enable_webapp"` + EnableDatatx bool `docs:"false;Whether data transfers are enabled in OCM shares." mapstructure:"enable_datatx"` +} + +type OcmDiscoveryData struct { + Enabled bool `json:"enabled" xml:"enabled"` + APIVersion string `json:"apiVersion" xml:"apiVersion"` + Endpoint string `json:"endPoint" xml:"endPoint"` + Provider string `json:"provider" xml:"provider"` + ResourceTypes []resourceTypes `json:"resourceTypes" xml:"resourceTypes"` + Capabilities []string `json:"capabilities" xml:"capabilities"` +} + +type resourceTypes struct { + Name string `json:"name"` + ShareTypes []string `json:"shareTypes"` + Protocols map[string]string `json:"protocols"` +} + +type wkocmHandler struct { + data *OcmDiscoveryData +} + +func (c *OcmProviderConfig) ApplyDefaults() { + if c.OCMPrefix == "" { + c.OCMPrefix = "ocm" + } + if c.Provider == "" { + c.Provider = "reva" + } + if c.WebdavRoot == "" { + c.WebdavRoot = "/remote.php/dav/ocm/" + } + if c.WebdavRoot[len(c.WebdavRoot)-1:] != "/" { + c.WebdavRoot += "/" + } + if c.WebappRoot == "" { + c.WebappRoot = "/external/sciencemesh/" + } + if c.WebappRoot[len(c.WebappRoot)-1:] != "/" { + c.WebappRoot += "/" + } +} + +func (h *wkocmHandler) init(c *OcmProviderConfig) { + // generates the (static) data structure to be exposed by /.well-known/ocm: + // first prepare an empty and disabled payload + c.ApplyDefaults() + d := &OcmDiscoveryData{} + d.Enabled = false + d.Endpoint = "" + d.APIVersion = OCMAPIVersion + d.Provider = c.Provider + d.ResourceTypes = []resourceTypes{{ + Name: "file", + ShareTypes: []string{}, + Protocols: map[string]string{}, + }} + d.Capabilities = []string{} + + if c.Endpoint == "" { + h.data = d + return + } + + endpointURL, err := url.Parse(c.Endpoint) + if err != nil { + h.data = d + return + } + + // now prepare the enabled one + d.Enabled = true + d.Endpoint, _ = url.JoinPath(c.Endpoint, c.OCMPrefix) + rtProtos := map[string]string{} + // webdav is always enabled + rtProtos["webdav"] = filepath.Join(endpointURL.Path, c.WebdavRoot) + if c.EnableWebapp { + rtProtos["webapp"] = filepath.Join(endpointURL.Path, c.WebappRoot) + } + if c.EnableDatatx { + rtProtos["datatx"] = filepath.Join(endpointURL.Path, c.WebdavRoot) + } + d.ResourceTypes = []resourceTypes{{ + Name: "file", // so far we only support `file` + ShareTypes: []string{"user"}, // so far we only support `user` + Protocols: rtProtos, // expose the protocols as per configuration + }} + // for now we hardcode the capabilities, as this is currently only advisory + d.Capabilities = []string{"/invite-accepted"} + h.data = d +} + +// This handler implements the OCM discovery endpoint specified in +// https://cs3org.github.io/OCM-API/docs.html?repo=OCM-API&user=cs3org#/paths/~1ocm-provider/get +func (h *wkocmHandler) Ocm(w http.ResponseWriter, r *http.Request) { + log := appctx.GetLogger(r.Context()) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + if r.UserAgent() == "Nextcloud Server Crawler" { + // Nextcloud decided to only support OCM 1.0 and 1.1, not any 1.x as per SemVer. See + // https://github.com/nextcloud/server/pull/39574#issuecomment-1679191188 + h.data.APIVersion = "1.1" + } else { + h.data.APIVersion = OCMAPIVersion + } + indented, _ := json.MarshalIndent(h.data, "", " ") + if _, err := w.Write(indented); err != nil { + log.Err(err).Msg("Error writing to ResponseWriter") + } +} diff --git a/internal/http/services/wellknown/openidconfiguration.go b/internal/http/services/wellknown/openidconfiguration.go deleted file mode 100644 index 15a8385e22..0000000000 --- a/internal/http/services/wellknown/openidconfiguration.go +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2018-2021 CERN -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// In applying this license, CERN does not waive the privileges and immunities -// granted to it by virtue of its status as an Intergovernmental Organization -// or submit itself to any jurisdiction. - -package wellknown - -import ( - "encoding/json" - "net/http" - - "github.com/cs3org/reva/v2/pkg/appctx" -) - -func (s *svc) doOpenidConfiguration(w http.ResponseWriter, r *http.Request) { - log := appctx.GetLogger(r.Context()) - pm := &ProviderMetadata{ - Issuer: s.conf.Issuer, - AuthorizationEndpoint: s.conf.AuthorizationEndpoint, - JwksURI: s.conf.JwksURI, - TokenEndpoint: s.conf.TokenEndpoint, - RevocationEndpoint: s.conf.RevocationEndpoint, - IntrospectionEndpoint: s.conf.IntrospectionEndpoint, - UserinfoEndpoint: s.conf.UserinfoEndpoint, - EndSessionEndpoint: s.conf.EndSessionEndpoint, - } - - b, err := json.Marshal(pm) - if err != nil { - log.Error().Err(err).Msg("error getting grpc client") - w.WriteHeader(http.StatusInternalServerError) - return - } - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Content-Type", "application/json") - _, err = w.Write(b) - if err != nil { - log.Error().Err(err).Msg("Error writing response") - return - } -} - -// The ProviderMetadata describes an idp. -// see https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata -// TODO(labkode): do we really need it to validate the token and get user claims? -type ProviderMetadata struct { - AuthorizationEndpoint string `json:"authorization_endpoint,omitempty"` - // claims_parameter_supported - ClaimsSupported []string `json:"claims_supported,omitempty"` - // grant_types_supported - IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"` - Issuer string `json:"issuer,omitempty"` - JwksURI string `json:"jwks_uri,omitempty"` - // registration_endpoint - // request_object_signing_alg_values_supported - // request_parameter_supported - // request_uri_parameter_supported - // require_request_uri_registration - // response_modes_supported - ResponseTypesSupported []string `json:"response_types_supported,omitempty"` - ScopesSupported []string `json:"scopes_supported,omitempty"` - SubjectTypesSupported []string `json:"subject_types_supported,omitempty"` - TokenEndpoint string `json:"token_endpoint,omitempty"` - // token_endpoint_auth_methods_supported - // token_endpoint_auth_signing_alg_values_supported - UserinfoEndpoint string `json:"userinfo_endpoint,omitempty"` - // userinfo_signing_alg_values_supported - // code_challenge_methods_supported - IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"` - // introspection_endpoint_auth_methods_supported - // introspection_endpoint_auth_signing_alg_values_supported - RevocationEndpoint string `json:"revocation_endpoint,omitempty"` - // revocation_endpoint_auth_methods_supported - // revocation_endpoint_auth_signing_alg_values_supported - // id_token_encryption_alg_values_supported - // id_token_encryption_enc_values_supported - // userinfo_encryption_alg_values_supported - // userinfo_encryption_enc_values_supported - // request_object_encryption_alg_values_supported - // request_object_encryption_enc_values_supported - CheckSessionIframe string `json:"check_session_iframe,omitempty"` - EndSessionEndpoint string `json:"end_session_endpoint,omitempty"` - // claim_types_supported -} diff --git a/internal/http/services/wellknown/webfinger.go b/internal/http/services/wellknown/webfinger.go deleted file mode 100644 index 8d4ffc91e9..0000000000 --- a/internal/http/services/wellknown/webfinger.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2018-2021 CERN -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// In applying this license, CERN does not waive the privileges and immunities -// granted to it by virtue of its status as an Intergovernmental Organization -// or submit itself to any jurisdiction. - -package wellknown - -import ( - "net/http" -) - -func (s *svc) doWebfinger(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNotFound) -} diff --git a/internal/http/services/wellknown/wellknown.go b/internal/http/services/wellknown/wellknown.go index 8083928ace..97210697db 100644 --- a/internal/http/services/wellknown/wellknown.go +++ b/internal/http/services/wellknown/wellknown.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2024 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -23,8 +23,8 @@ import ( "github.com/cs3org/reva/v2/pkg/appctx" "github.com/cs3org/reva/v2/pkg/rhttp/global" - "github.com/cs3org/reva/v2/pkg/rhttp/router" - "github.com/mitchellh/mapstructure" + "github.com/cs3org/reva/v2/pkg/utils/cfg" + "github.com/go-chi/chi/v5" "github.com/rs/zerolog" ) @@ -32,76 +32,60 @@ func init() { global.Register("wellknown", New) } -type config struct { - Prefix string `mapstructure:"prefix"` - Issuer string `mapstructure:"issuer"` - AuthorizationEndpoint string `mapstructure:"authorization_endpoint"` - JwksURI string `mapstructure:"jwks_uri"` - TokenEndpoint string `mapstructure:"token_endpoint"` - RevocationEndpoint string `mapstructure:"revocation_endpoint"` - IntrospectionEndpoint string `mapstructure:"introspection_endpoint"` - UserinfoEndpoint string `mapstructure:"userinfo_endpoint"` - EndSessionEndpoint string `mapstructure:"end_session_endpoint"` -} - -func (c *config) init() { - if c.Prefix == "" { - c.Prefix = ".well-known" - } +type svc struct { + router chi.Router + Conf *config } -type svc struct { - conf *config - handler http.Handler +type config struct { + OCMProvider OcmProviderConfig `mapstructure:"ocmprovider"` } -// New returns a new webuisvc +// New returns a new wellknown object. func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) { - conf := &config{} - if err := mapstructure.Decode(m, conf); err != nil { + var c config + if err := cfg.Decode(m, &c); err != nil { return nil, err } - conf.init() - + r := chi.NewRouter() s := &svc{ - conf: conf, + router: r, + Conf: &c, + } + if err := s.routerInit(); err != nil { + return nil, err } - s.setHandler() + return s, nil } -func (s *svc) Close() error { +func (s *svc) routerInit() error { + wkocmHandler := new(wkocmHandler) + wkocmHandler.init(&s.Conf.OCMProvider) + s.router.Get("/ocm", wkocmHandler.Ocm) return nil } -func (s *svc) Prefix() string { - return s.conf.Prefix +func (s *svc) Close() error { + return nil } -func (s *svc) Handler() http.Handler { - return s.handler +func (s *svc) Prefix() string { + return ".well-known" } func (s *svc) Unprotected() []string { - return []string{ - "/openid-configuration", - } + return []string{"/", "/ocm"} } -func (s *svc) setHandler() { - s.handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +func (s *svc) Handler() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log := appctx.GetLogger(r.Context()) - var head string - head, r.URL.Path = router.ShiftPath(r.URL.Path) - log.Info().Msgf("wellknown routing: head=%s tail=%s", head, r.URL.Path) - switch head { - case "webfinger": - s.doWebfinger(w, r) - case "openid-configuration": - s.doOpenidConfiguration(w, r) - default: - w.WriteHeader(http.StatusNotFound) - } + log.Debug().Str("path", r.URL.Path).Msg(".well-known routing") + + // unset raw path, otherwise chi uses it to route and then fails to match percent encoded path segments + r.URL.RawPath = "" + s.router.ServeHTTP(w, r) }) }