diff --git a/cmd/reva/gen/gen.go b/cmd/reva/gen/gen.go index ff42ff8888..5703e8e3e2 100644 --- a/cmd/reva/gen/gen.go +++ b/cmd/reva/gen/gen.go @@ -375,12 +375,16 @@ func WriteConfig(p string, cs string, dd string, dp string) { var usersTemplate = `[{{range $i, $e := .}}{{if $i}},{{end}} { + "id": { + "idp": "{{$e.Iss}}", + "opaque_id": "{{$e.Sub}}", + }, "sub": "{{$e.Sub}}", "iss": "{{$e.Iss}}", "username": "{{$e.Username}}", "secret": "{{$e.Secret}}", "mail": "{{$e.Mail}}", - "displayname": "{{$e.Displayname}}" + "display_name": "{{$e.Displayname}}" }{{end}} ] ` diff --git a/cmd/revad/gateway.toml b/cmd/revad/gateway.toml index 9f13feb0fc..091dd98387 100644 --- a/cmd/revad/gateway.toml +++ b/cmd/revad/gateway.toml @@ -42,5 +42,4 @@ secret = "Pive-Fumkiu4" prefix = "owncloud" chunk_folder = "/var/tmp/revad/chunks" gatewaysvc = "localhost:10000" -enable_cors = true diff --git a/cmd/revad/revad.toml b/cmd/revad/revad.toml index 7ee1d8027b..3896a67821 100644 --- a/cmd/revad/revad.toml +++ b/cmd/revad/revad.toml @@ -43,7 +43,6 @@ prefix = "ui" prefix = "owncloud" chunk_folder = "/var/tmp/revad/chunks" gatewaysvc = "localhost:9999" -enable_cors = true [http.services.datasvc] driver = "local" diff --git a/cmd/revad/svcs/grpcsvcs/authsvc/authsvc.go b/cmd/revad/svcs/grpcsvcs/authsvc/authsvc.go index ad3e7518d0..3067412ff9 100644 --- a/cmd/revad/svcs/grpcsvcs/authsvc/authsvc.go +++ b/cmd/revad/svcs/grpcsvcs/authsvc/authsvc.go @@ -134,7 +134,7 @@ func (s *service) GenerateAccessToken(ctx context.Context, req *authv0alphapb.Ge ctx, err := s.authmgr.Authenticate(ctx, username, password) if err != nil { - log.Error().Err(err).Msg("error authentication user") + log.Error().Err(err).Msg("error authenticating user") status := &rpcpb.Status{Code: rpcpb.Code_CODE_UNAUTHENTICATED} res := &authv0alphapb.GenerateAccessTokenResponse{Status: status} return res, nil diff --git a/cmd/revad/svcs/grpcsvcs/status/status.go b/cmd/revad/svcs/grpcsvcs/status/status.go index 2525a1f5c4..c85673af4c 100644 --- a/cmd/revad/svcs/grpcsvcs/status/status.go +++ b/cmd/revad/svcs/grpcsvcs/status/status.go @@ -63,6 +63,15 @@ func NewUnauthenticated(ctx context.Context, msg string) *rpcpb.Status { } } +// NewUnimplemented returns a Status with CODE_UNIMPLEMENTED. +func NewUnimplemented(ctx context.Context, msg string) *rpcpb.Status { + return &rpcpb.Status{ + Code: rpcpb.Code_CODE_UNIMPLEMENTED, + Message: msg, + Trace: getTrace(ctx), + } +} + func getTrace(ctx context.Context) string { span := trace.FromContext(ctx) return span.SpanContext().TraceID.String() diff --git a/cmd/revad/svcs/grpcsvcs/usershareprovidersvc/usershareprovidersvc.go b/cmd/revad/svcs/grpcsvcs/usershareprovidersvc/usershareprovidersvc.go index 67f53b6dbc..c012e39cc7 100644 --- a/cmd/revad/svcs/grpcsvcs/usershareprovidersvc/usershareprovidersvc.go +++ b/cmd/revad/svcs/grpcsvcs/usershareprovidersvc/usershareprovidersvc.go @@ -147,6 +147,7 @@ func (s *service) GetShare(ctx context.Context, req *usershareproviderv0alphapb. func (s *service) ListShares(ctx context.Context, req *usershareproviderv0alphapb.ListSharesRequest) (*usershareproviderv0alphapb.ListSharesResponse, error) { log := appctx.GetLogger(ctx) + shares, err := s.sm.ListShares(ctx, req.Filters) // TODO(labkode): add filter to share manager if err != nil { log.Err(err).Msg("error listing shares") diff --git a/cmd/revad/svcs/httpsvcs/ocdavsvc/avatar.go b/cmd/revad/svcs/httpsvcs/ocdavsvc/avatar.go deleted file mode 100644 index 4f044b135a..0000000000 --- a/cmd/revad/svcs/httpsvcs/ocdavsvc/avatar.go +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2018-2019 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 ocdavsvc - -import ( - "encoding/hex" - "net/http" - - "github.com/cs3org/reva/cmd/revad/svcs/httpsvcs" - "github.com/cs3org/reva/pkg/appctx" -) - -func (s *svc) doAvatar(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - log := appctx.GetLogger(ctx) - - if r.Method == "OPTIONS" { - // no need for the user, and we need to be able - // to answer preflight checks, which have no auth headers - r.URL.Path = "/" // always use / ... we just want the options answered so phoenix doesnt hiccup - s.doOptions(w, r) - return - } - - _, r.URL.Path = httpsvcs.ShiftPath(r.URL.Path) - if r.Method == "GET" && r.URL.Path == "/128.png" { - // TODO load avatar url from user context? - const img = "89504E470D0A1A0A0000000D4948445200000080000000800806000000C33E61CB00000006624B474400FF00FF00FFA0BDA793000000097048597300000B1300000B1301009A9C180000000774494D4507E3061B080516D3ECF61E000008F24944415478DAED9D7D8C1D5515C07FDB76774B775BB7454AA54BBB2D5DDD765B6BD34A140B464CB07EA0113518016B4848FC438A1A448D9A18FF40316942524D544C900F49A17C2882120A28604D506C915AB160B7B2A14275B7BB606BCB76779F7FDC79F4CE79F7BD7DEFED7B3377E69E5FF2B233DBED9B7B3EEECCB977CE3D171445511445511445098B9680645D0BAC8C7EAE020A0E5D0C027B80DDC033EA1ED96521B001D80A3C1F19BB9ECF007003F0CEE83B15CFB90C781A189986D1CB7D8E007F06AE5035FBC599C0359181AA35E6716014188A3EA3D1EFAAFDFFAF025F06DEA2EA4F97EB81935318EB047037F0396035300FE8043A8039D1A723FADD3CA01FB80AB817989CE2BB4F0237AA1992E703C00B150CB313D812057DD36555D4DB7756B8DE41E0236A9664B8A982216E897A72B3980BDC5CE1CE70AB9AA779744541984BF1DF03BA136C4B77F4F871B5E519E074355763590E8C9519A62D4DB15DDDC07E47BBC681156AB6C6D0071C7328F93A60A607ED9B017CDED1BEA35140A94C83259122ED67EE316093876DDD28E61F26A3B69EAD66AC9F61D1AB463D1F7BCF075E126D1E5233D6C74EC7E4CBEA0CB47B317048B4FD6135676D5C2E14F83A705686DA3FD771F7D229E41A823E19507D2A83729CEF90A34FCD3B35F70BA5DD906159AE14B2FC5ACD5B99F384C20E016D19966726B04FC874819AB93C434259EFCD814C2B1C2319C5C14542513FCF916C5B856C17ABB94BF915F1A9D43CCDA2AD20FEDAFA5135779CD9A287FC2D8732EE12322E52B39FE28742391B722863BF90F17635BBA115386C296630C7B2DA492CFFC16423A5CA0C0F94B214938A55E4DE9CC73945E691EEAB6C6F1C605D140314F96D8E1DE009EBB82D923D78EE14CFC63C67DA9E2D64DDA1E687D7882751E49D717452E80DE692DFC99F723C26646E0F390638579C3F1280033CEE888182758035E27C57000EF09438EF0BD9017AC5F940000EB0479CF784EC004BACE362E66FDE1916E7DD213BC07CEBF8BF8104BE72B4B330640768B58E8F0734FA39661D7785EA002DE2FA2703790448676F0DD901EC123593013D02267CB90BCF48591105E110A13051A12304E500E3BEDC0A136666858E105410683B407B20778116605699BB41700E30621DCF09E80E709A757C22640778D93A9E1B501C603BFB70C80EF092753C0B3FD6FB27815DC6E65F213B80CCFFEB0DC0F8B27CCC3F43768003E27C6D000E20339E5F08D9019E9B423979E43C71BE97C0B1B3639E0A40DE3F089983E72FC4EBEAE41DBBDED1F36937C687B4703B55BA050F72E59B488F18EA3EAE0E509A07B826C70E2083DC87D5014C143C669DAFCFB103D8B28D3B82E020E9225EEA3DCF2B839EB4E41C414BCABEC19E4022635BC67D3E346886278AF99138BF3487C6DF2CCE7FA2FD3EEE8876EF78368732CA6251AD6AF6D2D180BDA54B9E6AEC2E25BE25CD633EF53C5FD86E1DCF06DE9D2307D8487C09FC1DDADF4B5981C98E29F692277224DB1F2DB926D0BD04CAF2AC784E2ECB814CB236D05E3573792E10CABA270732FD46C874A19AB9320396B286C9F664C9424C1188A23C2FFA38FCF20D3B185C80D9222EAB7C0C7893757EA7F6EFA9E9A174E3C7AC22B797D3E0AF4AEE168AFB520665F8AA90E101356BF57489DEB39F6C958D6FA77467D337AB59ABA705784828F033196AFF15A2ED8F12D6DAC786B086D22D57B2B07A688EA3DDEBD59CF5F103A1C86D1968F336D1E69FAA19EB6701A6744C5666079789B61ED367FFF4F99650EA11FC5C42D64A3CB3A9007C57CDD7189E168AFDBE876DBC91FCE734A4463F66F3485BC11FF4A87D978AB68D11C632B744B99AD2DD44CFF1A05DEB89BFC62E00D7AAB99AC30EA1E8D7800E8F82BE02709F9AA9799C46E9DE820748A7E2F65B8997BA2F06A81D6AA6E6D289C9A9B7153F98F070EB3D8E9E3F4AFCCD9FD244563B0C3044325BB17DC271ED02F02E354BF2C1D70987219AB9A6E0DA32C6FFA49A231DFACA18647B13AE7553996B6D5333A4CB324CA125DB2813C0CA065EA3D731D42B00B7A9FAFDC136CCFF68ECEE638BA2EF94A38F3655BB1FC8F705CDD87CF23E718D6FAADAFD19168E0AE3346338D625AE314C7CB58F921232FBA6995BCFDD21AEF551557FBAB4736AA38924B26F36503AF9A3A95E29F26002C33F89CC58BE4BCD900E1FA2741E3E89A8BC8D78E2C704F03E3547B2F43AC6E4572778FD2D8EEBF7A859926101F04A0AB77E89DCF5FC1029EF0016024B89EFBE5D00FE413AAF83DB319341765B4E92EF4297A97215A519C2C749E60D603916112FFD52DC14F2323557633803F3EEFD49C73377043F52C2CE1141617149DB4398323767AA19AB6739F005E09798248F51DC6FE00EE357DD80D3817F9769EBAB517CF040143C6AB018B10CB818F80EA61ED0781905CACF0EFC4CBBEAC45434A9468613983AC15F073691AF8A6815E9C1E4CF8F44069FAC5261C5D2EAABF07BE6AD0593A3F05C0D724D46BA18C2AC77E8CE93C1DB804F03B746B7F4420D9F21E07EE02BC0BA0CCADE0F5C8399391CA851F641E076E072329864DA1605463B6A10780CB38E6E2F701D8D7D97EF13E702D7037F8F460B6355EAE741E06D789E7FB004933675A04AA186819B31397C6B896FA516029D98E4D64B22BD1DA9426703C08F7DEA20B380B7535A0ACDF59C3B0CEC06BE019CA531B0933E4C8EE100A51948AE6078252916FADA8829803C51A191AF005FC32CA298AFF6ADA9632D8E628017A77874EE8E3A6162F402BFA8D0A8039852E8FD6AC786B10E938CF27205BD6F4F628EE18B94AED22D7E0E621226B40C7AF368053E4EE9CA287B7E6173332EDC8149B4745DF477C087D53689B3391A4DB86C720B0DAEA774D07191A3C0F96A87D4D952663839D8A85BCE2EC7977F9B6C54EC0A851EE0670E3BED9EEEDCC1FB1D51E73AD5B7B75CE888D12E99CE17CAE95B5D04E93F17519A2B5917B2FAC53ED56D6678840614A9FEACF8924DAAD7CCB0BEDA3B77A569C47788F3DFAB5E33C37E71BEB81E07586E1D1F45F7BACF1AF67ECC67D4E30036AFAB03648A494CB26A91B28B6567D5F0A573D07570596176E40045C3774ED7011670EA3DBFE23F2DC2E8EDF538408B389EA77ACD2C6DF5C40007556FB9E1AFD5F472175762B66D9B2D6EFF05F19332E7D4F877AE7F6FF66327EF8FB53F015BB50F288AA2288AA2288A62F83FEC37068C6750398B0000000049454E44AE426082" - decoded, err := hex.DecodeString(img) - if err != nil { - log.Error().Err(err).Msg("error decoding string") - w.WriteHeader(http.StatusInternalServerError) - } - w.Header().Set("Content-Type", "image/png") - if _, err := w.Write(decoded); err != nil { - log.Error().Err(err).Msg("error writing data response") - } - return - } - - w.WriteHeader(http.StatusNotFound) -} diff --git a/cmd/revad/svcs/httpsvcs/ocdavsvc/avatars.go b/cmd/revad/svcs/httpsvcs/ocdavsvc/avatars.go new file mode 100644 index 0000000000..b10ce10640 --- /dev/null +++ b/cmd/revad/svcs/httpsvcs/ocdavsvc/avatars.go @@ -0,0 +1,69 @@ +// Copyright 2018-2019 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 ocdavsvc + +import ( + "encoding/hex" + "net/http" + + "github.com/cs3org/reva/cmd/revad/svcs/httpsvcs" + "github.com/cs3org/reva/pkg/appctx" +) + +// AvatarsHandler handles avatar requests +type AvatarsHandler struct { +} + +func (h *AvatarsHandler) init(c *Config) error { + return nil +} + +// Handler handles requests +func (h *AvatarsHandler) Handler(s *svc) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := appctx.GetLogger(ctx) + + if r.Method == "OPTIONS" { + // no need for the user, and we need to be able + // to answer preflight checks, which have no auth headers + r.URL.Path = "/" // always use / ... we just want the options answered so phoenix doesnt hiccup + s.doOptions(w, r) + return + } + + _, r.URL.Path = httpsvcs.ShiftPath(r.URL.Path) + if r.Method == "GET" && r.URL.Path == "/128.png" { + // TODO load avatar url from user context? + const img = "89504E470D0A1A0A0000000D4948445200000080000000800806000000C33E61CB00000006624B474400FF00FF00FFA0BDA793000000097048597300000B1300000B1301009A9C180000000774494D4507E3061B080516D3ECF61E000008F24944415478DAED9D7D8C1D5515C07FDB76774B775BB7454AA54BBB2D5DDD765B6BD34A140B464CB07EA0113518016B4848FC438A1A448D9A18FF40316942524D544C900F49A17C2882120A28604D506C915AB160B7B2A14275B7BB606BCB76779F7FDC79F4CE79F7BD7DEFED7B3377E69E5FF2B233DBED9B7B3EEECCB977CE3D171445511445511445098B9680645D0BAC8C7EAE020A0E5D0C027B80DDC033EA1ED96521B001D80A3C1F19BB9ECF007003F0CEE83B15CFB90C781A189986D1CB7D8E007F06AE5035FBC599C0359181AA35E6716014188A3EA3D1EFAAFDFFAF025F06DEA2EA4F97EB81935318EB047037F0396035300FE8043A8039D1A723FADD3CA01FB80AB817989CE2BB4F0237AA1992E703C00B150CB313D812057DD36555D4DB7756B8DE41E0236A9664B8A982216E897A72B3980BDC5CE1CE70AB9AA779744541984BF1DF03BA136C4B77F4F871B5E519E074355763590E8C9519A62D4DB15DDDC07E47BBC681156AB6C6D0071C7328F93A60A607ED9B017CDED1BEA35140A94C83259122ED67EE316093876DDD28E61F26A3B69EAD66AC9F61D1AB463D1F7BCF075E126D1E5233D6C74EC7E4CBEA0CB47B317048B4FD6135676D5C2E14F83A705686DA3FD771F7D229E41A823E19507D2A83729CEF90A34FCD3B35F70BA5DD906159AE14B2FC5ACD5B99F384C20E016D19966726B04FC874819AB93C434259EFCD814C2B1C2319C5C14542513FCF916C5B856C17ABB94BF915F1A9D43CCDA2AD20FEDAFA5135779CD9A287FC2D8732EE12322E52B39FE28742391B722863BF90F17635BBA115386C296630C7B2DA492CFFC16423A5CA0C0F94B214938A55E4DE9CC73945E691EEAB6C6F1C605D140314F96D8E1DE009EBB82D923D78EE14CFC63C67DA9E2D64DDA1E687D7882751E49D717452E80DE692DFC99F723C26646E0F390638579C3F1280033CEE888182758035E27C57000EF09438EF0BD9017AC5F940000EB0479CF784EC004BACE362E66FDE1916E7DD213BC07CEBF8BF8104BE72B4B330640768B58E8F0734FA39661D7785EA002DE2FA2703790448676F0DD901EC123593013D02267CB90BCF48591105E110A13051A12304E500E3BEDC0A136666858E105410683B407B20778116605699BB41700E30621DCF09E80E709A757C22640778D93A9E1B501C603BFB70C80EF092753C0B3FD6FB27815DC6E65F213B80CCFFEB0DC0F8B27CCC3F43768003E27C6D000E20339E5F08D9019E9B423979E43C71BE97C0B1B3639E0A40DE3F089983E72FC4EBEAE41DBBDED1F36937C687B4703B55BA050F72E59B488F18EA3EAE0E509A07B826C70E2083DC87D5014C143C669DAFCFB103D8B28D3B82E020E9225EEA3DCF2B839EB4E41C414BCABEC19E4022635BC67D3E346886278AF99138BF3487C6DF2CCE7FA2FD3EEE8876EF78368732CA6251AD6AF6D2D180BDA54B9E6AEC2E25BE25CD633EF53C5FD86E1DCF06DE9D2307D8487C09FC1DDADF4B5981C98E29F692277224DB1F2DB926D0BD04CAF2AC784E2ECB814CB236D05E3573792E10CABA270732FD46C874A19AB9320396B286C9F664C9424C1188A23C2FFA38FCF20D3B185C80D9222EAB7C0C7893757EA7F6EFA9E9A174E3C7AC22B797D3E0AF4AEE168AFB520665F8AA90E101356BF57489DEB39F6C958D6FA77467D337AB59ABA705784828F033196AFF15A2ED8F12D6DAC786B086D22D57B2B07A688EA3DDEBD59CF5F103A1C86D1968F336D1E69FAA19EB6701A6744C5666079789B61ED367FFF4F99650EA11FC5C42D64A3CB3A9007C57CDD7189E168AFDBE876DBC91FCE734A4463F66F3485BC11FF4A87D978AB68D11C632B744B99AD2DD44CFF1A05DEB89BFC62E00D7AAB99AC30EA1E8D7800E8F82BE02709F9AA9799C46E9DE820748A7E2F65B8997BA2F06A81D6AA6E6D289C9A9B7153F98F070EB3D8E9E3F4AFCCD9FD244563B0C3044325BB17DC271ED02F02E354BF2C1D70987219AB9A6E0DA32C6FFA49A231DFACA18647B13AE7553996B6D5333A4CB324CA125DB2813C0CA065EA3D731D42B00B7A9FAFDC136CCFF68ECEE638BA2EF94A38F3655BB1FC8F705CDD87CF23E718D6FAADAFD19168E0AE3346338D625AE314C7CB58F921232FBA6995BCFDD21AEF551557FBAB4736AA38924B26F36503AF9A3A95E29F26002C33F89CC58BE4BCD900E1FA2741E3E89A8BC8D78E2C704F03E3547B2F43AC6E4572778FD2D8EEBF7A859926101F04A0AB77E89DCF5FC1029EF0016024B89EFBE5D00FE413AAF83DB319341765B4E92EF4297A97215A519C2C749E60D603916112FFD52DC14F2323557633803F3EEFD49C73377043F52C2CE1141617149DB4398323767AA19AB6739F005E09798248F51DC6FE00EE357DD80D3817F9769EBAB517CF040143C6AB018B10CB818F80EA61ED0781905CACF0EFC4CBBEAC45434A9468613983AC15F073691AF8A6815E9C1E4CF8F44069FAC5261C5D2EAABF07BE6AD0593A3F05C0D724D46BA18C2AC77E8CE93C1DB804F03B746B7F4420D9F21E07EE02BC0BA0CCADE0F5C8399391CA851F641E076E072329864DA1605463B6A10780CB38E6E2F701D8D7D97EF13E702D7037F8F460B6355EAE741E06D789E7FB004933675A04AA186819B31397C6B896FA516029D98E4D64B22BD1DA9426703C08F7DEA20B380B7535A0ACDF59C3B0CEC06BE019CA531B0933E4C8EE100A51948AE6078252916FADA8829803C51A191AF005FC32CA298AFF6ADA9632D8E628017A77874EE8E3A6162F402BFA8D0A8039852E8FD6AC786B10E938CF27205BD6F4F628EE18B94AED22D7E0E621226B40C7AF368053E4EE9CA287B7E6173332EDC8149B4745DF477C087D53689B3391A4DB86C720B0DAEA774D07191A3C0F96A87D4D952663839D8A85BCE2EC7977F9B6C54EC0A851EE0670E3BED9EEEDCC1FB1D51E73AD5B7B75CE888D12E99CE17CAE95B5D04E93F17519A2B5917B2FAC53ED56D6678840614A9FEACF8924DAAD7CCB0BEDA3B77A569C47788F3DFAB5E33C37E71BEB81E07586E1D1F45F7BACF1AF67ECC67D4E30036AFAB03648A494CB26A91B28B6567D5F0A573D07570596176E40045C3774ED7011670EA3DBFE23F2DC2E8EDF538408B389EA77ACD2C6DF5C40007556FB9E1AFD5F472175762B66D9B2D6EFF05F19332E7D4F877AE7F6FF66327EF8FB53F015BB50F288AA2288AA2288A62F83FEC37068C6750398B0000000049454E44AE426082" + decoded, err := hex.DecodeString(img) + if err != nil { + log.Error().Err(err).Msg("error decoding string") + w.WriteHeader(http.StatusInternalServerError) + } + w.Header().Set("Content-Type", "image/png") + if _, err := w.Write(decoded); err != nil { + log.Error().Err(err).Msg("error writing data response") + } + return + } + + w.WriteHeader(http.StatusNotFound) + }) +} diff --git a/cmd/revad/svcs/httpsvcs/ocdavsvc/copy.go b/cmd/revad/svcs/httpsvcs/ocdavsvc/copy.go index b462b696a4..9d233decb7 100644 --- a/cmd/revad/svcs/httpsvcs/ocdavsvc/copy.go +++ b/cmd/revad/svcs/httpsvcs/ocdavsvc/copy.go @@ -186,9 +186,9 @@ func descend(ctx context.Context, client storageproviderv0alphapb.StorageProvide return fmt.Errorf("status code %d", res.Status.Code) } - for _, e := range res.Infos { - childDst := path.Join(dst, path.Base(e.Path)) - err := descend(ctx, client, e, childDst) + for i := range res.Infos { + childDst := path.Join(dst, path.Base(res.Infos[i].Path)) + err := descend(ctx, client, res.Infos[i], childDst) if err != nil { return err } diff --git a/cmd/revad/svcs/httpsvcs/ocdavsvc/dav.go b/cmd/revad/svcs/httpsvcs/ocdavsvc/dav.go new file mode 100644 index 0000000000..dad53c8271 --- /dev/null +++ b/cmd/revad/svcs/httpsvcs/ocdavsvc/dav.go @@ -0,0 +1,63 @@ +// Copyright 2018-2019 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 ocdavsvc + +import ( + "net/http" + + "github.com/cs3org/reva/cmd/revad/svcs/httpsvcs" +) + +// DavHandler routes to the different sub handlers +type DavHandler struct { + FilesHandler *FilesHandler + AvatarsHandler *AvatarsHandler + MetaHandler *MetaHandler +} + +func (h *DavHandler) init(c *Config) error { + h.FilesHandler = new(FilesHandler) + if err := h.FilesHandler.init(c); err != nil { + return err + } + h.AvatarsHandler = new(AvatarsHandler) + if err := h.AvatarsHandler.init(c); err != nil { + return err + } + h.MetaHandler = new(MetaHandler) + return h.MetaHandler.init(c) +} + +// Handler handles requests +func (h *DavHandler) Handler(s *svc) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var head string + head, r.URL.Path = httpsvcs.ShiftPath(r.URL.Path) + switch head { + case "files": + h.FilesHandler.Handler(s).ServeHTTP(w, r) + case "avatars": + h.AvatarsHandler.Handler(s).ServeHTTP(w, r) + case "meta": + h.MetaHandler.Handler(s).ServeHTTP(w, r) + default: + w.WriteHeader(http.StatusNotFound) + } + }) +} diff --git a/cmd/revad/svcs/httpsvcs/ocdavsvc/files.go b/cmd/revad/svcs/httpsvcs/ocdavsvc/files.go new file mode 100644 index 0000000000..f08c01064b --- /dev/null +++ b/cmd/revad/svcs/httpsvcs/ocdavsvc/files.go @@ -0,0 +1,76 @@ +// Copyright 2018-2019 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 ocdavsvc + +import ( + "context" + "net/http" + "path" +) + +// FilesHandler routes to the different sub handlers +type FilesHandler struct { +} + +func (h *FilesHandler) init(c *Config) error { + return nil +} + +// Handler handles requests +func (h *FilesHandler) Handler(s *svc) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + // webdav should be death: baseURI is encoded as part of the + // response payload in href field + baseURI := path.Join("/", s.Prefix(), "remote.php/dav/files") + ctx := context.WithValue(r.Context(), ctxKeyBaseURI, baseURI) + r = r.WithContext(ctx) + + switch r.Method { + case "PROPFIND": + s.doPropfind(w, r) + case http.MethodOptions: + s.doOptions(w, r) + case http.MethodHead: + s.doHead(w, r) + case http.MethodGet: + s.doGet(w, r) + case "LOCK": + s.doLock(w, r) + case "UNLOCK": + s.doUnlock(w, r) + case "PROPPATCH": + s.doProppatch(w, r) + case "MKCOL": + s.doMkcol(w, r) + case "MOVE": + s.doMove(w, r) + case "COPY": + s.doCopy(w, r) + case http.MethodPut: + s.doPut(w, r) + case http.MethodDelete: + s.doDelete(w, r) + case "REPORT": + s.doReport(w, r) + default: + w.WriteHeader(http.StatusNotFound) + } + }) +} diff --git a/cmd/revad/svcs/httpsvcs/ocdavsvc/get.go b/cmd/revad/svcs/httpsvcs/ocdavsvc/get.go index c7af490ecb..f79ebf805b 100644 --- a/cmd/revad/svcs/httpsvcs/ocdavsvc/get.go +++ b/cmd/revad/svcs/httpsvcs/ocdavsvc/get.go @@ -19,7 +19,6 @@ package ocdavsvc import ( - "fmt" "io" "net/http" "time" @@ -111,7 +110,7 @@ func (s *svc) doGet(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", info.MimeType) w.Header().Set("ETag", info.Etag) - w.Header().Set("OC-FileId", fmt.Sprintf("%s:%s", info.Id.StorageId, info.Id.OpaqueId)) + w.Header().Set("OC-FileId", wrapResourceID(info.Id)) w.Header().Set("OC-ETag", info.Etag) t := utils.TSToTime(info.Mtime) lastModifiedString := t.Format(time.RFC1123) diff --git a/cmd/revad/svcs/httpsvcs/ocdavsvc/head.go b/cmd/revad/svcs/httpsvcs/ocdavsvc/head.go index f187e8fb70..0e99a59afb 100644 --- a/cmd/revad/svcs/httpsvcs/ocdavsvc/head.go +++ b/cmd/revad/svcs/httpsvcs/ocdavsvc/head.go @@ -19,7 +19,6 @@ package ocdavsvc import ( - "fmt" "net/http" "time" @@ -61,7 +60,7 @@ func (s *svc) doHead(w http.ResponseWriter, r *http.Request) { info := res.Info w.Header().Set("Content-Type", info.MimeType) w.Header().Set("ETag", info.Etag) - w.Header().Set("OC-FileId", fmt.Sprintf("%s:%s", info.Id.StorageId, info.Id.OpaqueId)) + w.Header().Set("OC-FileId", wrapResourceID(info.Id)) w.Header().Set("OC-ETag", info.Etag) t := utils.TSToTime(info.Mtime) lastModifiedString := t.Format(time.RFC1123) diff --git a/cmd/revad/svcs/httpsvcs/ocdavsvc/meta.go b/cmd/revad/svcs/httpsvcs/ocdavsvc/meta.go new file mode 100644 index 0000000000..7b97ed85dc --- /dev/null +++ b/cmd/revad/svcs/httpsvcs/ocdavsvc/meta.go @@ -0,0 +1,60 @@ +// Copyright 2018-2019 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 ocdavsvc + +import ( + "net/http" + + "github.com/cs3org/reva/cmd/revad/svcs/httpsvcs" +) + +// MetaHandler handles meta requests +type MetaHandler struct { + VersionsHandler *VersionsHandler +} + +func (h *MetaHandler) init(c *Config) error { + h.VersionsHandler = new(VersionsHandler) + return h.VersionsHandler.init(c) +} + +// Handler handles requests +func (h *MetaHandler) Handler(s *svc) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + var id string + id, r.URL.Path = httpsvcs.ShiftPath(r.URL.Path) + if id == "" { + http.Error(w, "400 Bad Request", http.StatusBadRequest) + return + } + + did := unwrap(id) + + var head string + head, r.URL.Path = httpsvcs.ShiftPath(r.URL.Path) + switch head { + case "v": + h.VersionsHandler.Handler(s, did).ServeHTTP(w, r) + default: + w.WriteHeader(http.StatusNotFound) + } + + }) +} diff --git a/cmd/revad/svcs/httpsvcs/ocdavsvc/move.go b/cmd/revad/svcs/httpsvcs/ocdavsvc/move.go index 735cdb75a8..ca7becfdb8 100644 --- a/cmd/revad/svcs/httpsvcs/ocdavsvc/move.go +++ b/cmd/revad/svcs/httpsvcs/ocdavsvc/move.go @@ -19,7 +19,6 @@ package ocdavsvc import ( - "fmt" "net/http" "net/url" "path" @@ -195,7 +194,7 @@ func (s *svc) doMove(w http.ResponseWriter, r *http.Request) { info := dstStatRes.Info w.Header().Set("Content-Type", info.MimeType) w.Header().Set("ETag", info.Etag) - w.Header().Set("OC-FileId", fmt.Sprintf("%s:%s", info.Id.StorageId, info.Id.OpaqueId)) + w.Header().Set("OC-FileId", wrapResourceID(info.Id)) w.Header().Set("OC-ETag", info.Etag) w.WriteHeader(successCode) } diff --git a/cmd/revad/svcs/httpsvcs/ocdavsvc/ocdavsvc.go b/cmd/revad/svcs/httpsvcs/ocdavsvc/ocdavsvc.go index 92159ece27..deaadeccc9 100644 --- a/cmd/revad/svcs/httpsvcs/ocdavsvc/ocdavsvc.go +++ b/cmd/revad/svcs/httpsvcs/ocdavsvc/ocdavsvc.go @@ -19,18 +19,17 @@ package ocdavsvc import ( - "context" + "encoding/base64" + "fmt" "net/http" - "net/url" "os" - "path" + "strings" storageproviderv0alphapb "github.com/cs3org/go-cs3apis/cs3/storageprovider/v0alpha" "github.com/cs3org/reva/cmd/revad/httpserver" "github.com/cs3org/reva/cmd/revad/svcs/grpcsvcs/pool" "github.com/cs3org/reva/cmd/revad/svcs/httpsvcs" "github.com/cs3org/reva/pkg/appctx" - "github.com/cs3org/reva/pkg/user" "github.com/mitchellh/mapstructure" ) @@ -44,22 +43,22 @@ func init() { httpserver.Register("ocdavsvc", New) } -type config struct { +// Config holds the config options that need to be passed down to all ocdav handlers +type Config struct { Prefix string `mapstructure:"prefix"` ChunkFolder string `mapstructure:"chunk_folder"` GatewaySvc string `mapstructure:"gatewaysvc"` } type svc struct { - prefix string - chunkFolder string - handler http.Handler - gatewaySvc string + c *Config + webDavHandler *WebDavHandler + davHandler *DavHandler } // New returns a new ocdavsvc func New(m map[string]interface{}) (httpsvcs.Service, error) { - conf := &config{} + conf := &Config{} if err := mapstructure.Decode(m, conf); err != nil { return nil, err } @@ -73,28 +72,30 @@ func New(m map[string]interface{}) (httpsvcs.Service, error) { } s := &svc{ - prefix: conf.Prefix, - gatewaySvc: conf.GatewaySvc, - chunkFolder: conf.ChunkFolder, + c: conf, + webDavHandler: new(WebDavHandler), + davHandler: new(DavHandler), + } + // initialize handlers and set default configs + if err := s.webDavHandler.init(conf); err != nil { + return nil, err + } + if err := s.davHandler.init(conf); err != nil { + return nil, err } - s.setHandler() return s, nil } func (s *svc) Prefix() string { - return s.prefix -} - -func (s *svc) Handler() http.Handler { - return s.handler + return s.c.Prefix } func (s *svc) Close() error { return nil } -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()) // the webdav api is accessible from anywhere @@ -107,171 +108,28 @@ func (s *svc) setHandler() { return } - head, tail := httpsvcs.ShiftPath(r.URL.Path) - log.Debug().Str("head", head).Str("tail", tail).Msg("http routing") - + var head string + head, r.URL.Path = httpsvcs.ShiftPath(r.URL.Path) + log.Debug().Str("head", head).Str("tail", r.URL.Path).Msg("http routing") switch head { case "status.php": - r.URL.Path = tail s.doStatus(w, r) return case "remote.php": - head2, tail2 := httpsvcs.ShiftPath(tail) + var head2 string + head2, r.URL.Path = httpsvcs.ShiftPath(r.URL.Path) - // TODO(jfd): refactor as separate handler - // the old `webdav` endpoint uses remote.php/webdav/$path + // the old `/webdav` endpoint uses remote.php/webdav/$path if head2 == "webdav" { - - r.URL.Path = tail2 - - if r.Method == "OPTIONS" { - // no need for the user, and we need to be able - // to answer preflight checks, which have no auth headers - r.URL.Path = tail2 // tail alwas starts with / - s.doOptions(w, r) - return - } - - // inject username in path - ctx := r.Context() - u, ok := user.ContextGetUser(ctx) - if !ok { - log.Error().Msg("error getting user from context") - w.WriteHeader(http.StatusInternalServerError) - return - } - - // TODO(labkode): this assumes too much, basically using ocdavsvc you can't access a global namespace. - // This must be changed. - // r.URL.Path = path.Join("/", u.Username, tail2) - // webdav should be death: baseURI is encoded as part of the - // response payload in href field - baseURI := path.Join("/", s.Prefix(), "remote.php/webdav") - ctx = context.WithValue(r.Context(), ctxKeyBaseURI, baseURI) - - // inject username into Destination header if present - dstHeader := r.Header.Get("Destination") - if dstHeader != "" { - dstURL, err := url.ParseRequestURI(dstHeader) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - return - } - if dstURL.Path[:18] != "/remote.php/webdav" { - log.Warn().Str("path", dstURL.Path).Msg("dst needs to start with /remote.php/webdav/") - w.WriteHeader(http.StatusBadRequest) - return - } - r.Header.Set("Destination", path.Join(baseURI, u.Username, dstURL.Path[18:])) // 18 = len ("/remote.php/webdav") - } - - r = r.WithContext(ctx) - - switch r.Method { - case "PROPFIND": - s.doPropfind(w, r) - return - case "HEAD": - s.doHead(w, r) - return - case "GET": - s.doGet(w, r) - return - case "LOCK": - s.doLock(w, r) - return - case "UNLOCK": - s.doUnlock(w, r) - return - case "PROPPATCH": - s.doProppatch(w, r) - return - case "MKCOL": - s.doMkcol(w, r) - return - case "MOVE": - s.doMove(w, r) - return - case "COPY": - s.doCopy(w, r) - return - case "PUT": - s.doPut(w, r) - return - case "DELETE": - s.doDelete(w, r) - return - default: - log.Warn().Msg("resource not found") - w.WriteHeader(http.StatusNotFound) - return - } + s.webDavHandler.Handler(s).ServeHTTP(w, r) + return } - // TODO(jfd) refactor as separate handler - // the new `files` endpoint uses remote.php/dav/files/$user/$path style paths + // the new `/dav/files` endpoint uses remote.php/dav/files/$user/$path style paths if head2 == "dav" { - head3, tail3 := httpsvcs.ShiftPath(tail2) - if head3 == "files" { - r.URL.Path = tail3 - // webdav should be death: baseURI is encoded as part of the - // response payload in href field - baseURI := path.Join("/", s.Prefix(), "remote.php/dav/files") - ctx := context.WithValue(r.Context(), ctxKeyBaseURI, baseURI) - r = r.WithContext(ctx) - - switch r.Method { - case "PROPFIND": - s.doPropfind(w, r) - return - case "OPTIONS": - s.doOptions(w, r) - return - case "HEAD": - s.doHead(w, r) - return - case "GET": - s.doGet(w, r) - return - case "LOCK": - s.doLock(w, r) - return - case "UNLOCK": - s.doUnlock(w, r) - return - case "PROPPATCH": - s.doProppatch(w, r) - return - case "MKCOL": - s.doMkcol(w, r) - return - case "MOVE": - s.doMove(w, r) - return - case "COPY": - s.doCopy(w, r) - return - case "PUT": - s.doPut(w, r) - return - case "DELETE": - s.doDelete(w, r) - return - case "REPORT": - s.doReport(w, r) - return - default: - log.Warn().Msg("resource not found") - w.WriteHeader(http.StatusNotFound) - return - } - } - if head3 == "avatars" { - r.URL.Path = tail3 - s.doAvatar(w, r) - return - } + s.davHandler.Handler(s).ServeHTTP(w, r) + return } } log.Warn().Msg("resource not found") @@ -280,5 +138,32 @@ func (s *svc) setHandler() { } func (s *svc) getClient() (storageproviderv0alphapb.StorageProviderServiceClient, error) { - return pool.GetStorageProviderServiceClient(s.gatewaySvc) + return pool.GetStorageProviderServiceClient(s.c.GatewaySvc) +} + +func wrapResourceID(r *storageproviderv0alphapb.ResourceId) string { + return wrap(r.StorageId, r.OpaqueId) +} + +// The fileID must be encoded +// - XML safe, because it is going to be used in the profind result +// - url safe, because the id might be used in a url, eg. the /dav/meta nodes +// which is why we base62 encode it +func wrap(sid string, oid string) string { + return base64.URLEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", sid, oid))) +} + +func unwrap(rid string) *storageproviderv0alphapb.ResourceId { + decodedID, err := base64.URLEncoding.DecodeString(rid) + if err != nil { + return nil + } + parts := strings.SplitN(string(decodedID), ":", 2) + if len(parts) != 2 { + return nil + } + return &storageproviderv0alphapb.ResourceId{ + StorageId: parts[0], + OpaqueId: parts[1], + } } diff --git a/cmd/revad/svcs/httpsvcs/ocdavsvc/options.go b/cmd/revad/svcs/httpsvcs/ocdavsvc/options.go index a1c79a07d9..b7a48e1023 100644 --- a/cmd/revad/svcs/httpsvcs/ocdavsvc/options.go +++ b/cmd/revad/svcs/httpsvcs/ocdavsvc/options.go @@ -33,7 +33,7 @@ func (s *svc) doOptions(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Credentials", "true") w.Header().Set("Access-Control-Allow-Methods", allow) - w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, Depth, Ocs-Apirequest") + w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, Depth, Ocs-Apirequest, If-None-Match, Destination") w.Header().Set("Content-Type", "application/xml") w.Header().Set("Allow", allow) w.Header().Set("DAV", "1, 2") diff --git a/cmd/revad/svcs/httpsvcs/ocdavsvc/propfind.go b/cmd/revad/svcs/httpsvcs/ocdavsvc/propfind.go index e0716f3c71..d96d5f3f06 100644 --- a/cmd/revad/svcs/httpsvcs/ocdavsvc/propfind.go +++ b/cmd/revad/svcs/httpsvcs/ocdavsvc/propfind.go @@ -19,7 +19,6 @@ package ocdavsvc import ( - "bytes" "context" "encoding/xml" "fmt" @@ -105,7 +104,7 @@ func (s *svc) doPropfind(w http.ResponseWriter, r *http.Request) { infos = append(infos, res.Infos...) } - propRes, err := s.formatPropfind(ctx, fn, infos) + propRes, err := s.formatPropfind(ctx, infos) if err != nil { log.Error().Err(err).Msg("error formatting propfind") w.WriteHeader(http.StatusInternalServerError) @@ -149,10 +148,10 @@ func readPropfind(r io.Reader) (pf propfindXML, status int, err error) { return pf, 0, nil } -func (s *svc) formatPropfind(ctx context.Context, fn string, mds []*storageproviderv0alphapb.ResourceInfo) (string, error) { - responses := []*responseXML{} - for _, md := range mds { - res, err := s.mdToPropResponse(ctx, md) +func (s *svc) formatPropfind(ctx context.Context, mds []*storageproviderv0alphapb.ResourceInfo) (string, error) { + responses := make([]*responseXML, 0, len(mds)) + for i := range mds { + res, err := s.mdToPropResponse(ctx, mds[i]) if err != nil { return "", err } @@ -213,14 +212,7 @@ func (s *svc) mdToPropResponse(ctx context.Context, md *storageproviderv0alphapb getLastModified := s.newProp("d:getlastmodified", lasModifiedString) propList = append(propList, getLastModified) - // the fileID must be xml-escaped as there are cases like public links - // that contains a path as the file id. This path can contain &, for example, - // which if it is not encoded properly, will result in an empty view for the user - var fileIDEscaped bytes.Buffer - if err := xml.EscapeText(&fileIDEscaped, []byte(fmt.Sprintf("%s:%s", md.Id.StorageId, md.Id.OpaqueId))); err != nil { - return nil, err - } - ocID := s.newProp("oc:id", fileIDEscaped.String()) + ocID := s.newProp("oc:fileid", wrapResourceID(md.Id)) propList = append(propList, ocID) // PropStat, only HTTP/1.1 200 is sent. @@ -242,6 +234,7 @@ func (s *svc) mdToPropResponse(ctx context.Context, md *storageproviderv0alphapb err := errors.Wrap(errtypes.UserRequired("userrequired"), "error getting user from ctx") return nil, err } + // TODO can lead to slice out of bounds md.Path = md.Path[len(u.Username)+1:] } diff --git a/cmd/revad/svcs/httpsvcs/ocdavsvc/put.go b/cmd/revad/svcs/httpsvcs/ocdavsvc/put.go index 0da585af70..9d5f62210a 100644 --- a/cmd/revad/svcs/httpsvcs/ocdavsvc/put.go +++ b/cmd/revad/svcs/httpsvcs/ocdavsvc/put.go @@ -19,7 +19,6 @@ package ocdavsvc import ( - "fmt" "net/http" "regexp" "strconv" @@ -242,7 +241,7 @@ func (s *svc) doPut(w http.ResponseWriter, r *http.Request) { w.Header().Add("Content-Type", info2.MimeType) w.Header().Set("ETag", info2.Etag) - w.Header().Set("OC-FileId", fmt.Sprintf("%s:%s", info2.Id.StorageId, info2.Id.OpaqueId)) + w.Header().Set("OC-FileId", wrapResourceID(info2.Id)) w.Header().Set("OC-ETag", info2.Etag) t := utils.TSToTime(info2.Mtime) lastModifiedString := t.Format(time.RFC1123) diff --git a/cmd/revad/svcs/httpsvcs/ocdavsvc/putchunked.go b/cmd/revad/svcs/httpsvcs/ocdavsvc/putchunked.go index e0f61c6df5..6a40844dc9 100644 --- a/cmd/revad/svcs/httpsvcs/ocdavsvc/putchunked.go +++ b/cmd/revad/svcs/httpsvcs/ocdavsvc/putchunked.go @@ -74,7 +74,7 @@ func getChunkBLOBInfo(path string) (*chunkBLOBInfo, error) { } func (s *svc) createChunkTempFile() (string, *os.File, error) { - file, err := ioutil.TempFile(fmt.Sprintf("/%s", s.chunkFolder), "") + file, err := ioutil.TempFile(fmt.Sprintf("/%s", s.c.ChunkFolder), "") if err != nil { return "", nil, err } @@ -83,7 +83,7 @@ func (s *svc) createChunkTempFile() (string, *os.File, error) { } func (s *svc) getChunkFolderName(i *chunkBLOBInfo) (string, error) { - path := "/" + s.chunkFolder + filepath.Clean("/"+i.uploadID()) + path := "/" + s.c.ChunkFolder + filepath.Clean("/"+i.uploadID()) if err := os.MkdirAll(path, 0755); err != nil { return "", err } diff --git a/cmd/revad/svcs/httpsvcs/ocdavsvc/versions.go b/cmd/revad/svcs/httpsvcs/ocdavsvc/versions.go new file mode 100644 index 0000000000..16d3900f6a --- /dev/null +++ b/cmd/revad/svcs/httpsvcs/ocdavsvc/versions.go @@ -0,0 +1,219 @@ +// Copyright 2018-2019 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 ocdavsvc + +import ( + "context" + "net/http" + "path" + + rpcpb "github.com/cs3org/go-cs3apis/cs3/rpc" + storageproviderv0alphapb "github.com/cs3org/go-cs3apis/cs3/storageprovider/v0alpha" + typespb "github.com/cs3org/go-cs3apis/cs3/types" + "github.com/cs3org/reva/cmd/revad/svcs/httpsvcs" + "github.com/cs3org/reva/pkg/appctx" +) + +// VersionsHandler handles version requests +type VersionsHandler struct { +} + +func (h *VersionsHandler) init(c *Config) error { + return nil +} + +// Handler handles requests +// versions can be listed with a PROPFIND to /remote.php/dav/meta//v +// a version is identified by a timestamp, eg. /remote.php/dav/meta//v/1561410426 +func (h *VersionsHandler) Handler(s *svc, rid *storageproviderv0alphapb.ResourceId) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + // webdav should be death: baseURI is encoded as part of the + // response payload in href field + baseURI := path.Join("/", s.Prefix(), "remote.php/dav/meta", wrapResourceID(rid)) + ctx := context.WithValue(r.Context(), ctxKeyBaseURI, baseURI) + r = r.WithContext(ctx) + + var key string + key, r.URL.Path = httpsvcs.ShiftPath(r.URL.Path) + if r.Method == http.MethodOptions { + s.doOptions(w, r) + return + } + if key == "" && r.Method == "PROPFIND" { + h.doListVersions(w, r, s, rid) + return + } + if key != "" && r.Method == "COPY" { + // TODO(jfd) it seems we cannot directly GET version content with cs3 ... + // TODO(jfd) cs3api has no delete file version call + // TODO(jfd) restore version to given Destination, but cs3api has no destination + h.doRestore(w, r, s, rid, key) + return + } + + http.Error(w, "501 Forbidden", http.StatusNotImplemented) + }) +} + +func (h *VersionsHandler) doListVersions(w http.ResponseWriter, r *http.Request, s *svc, rid *storageproviderv0alphapb.ResourceId) { + ctx := r.Context() + log := appctx.GetLogger(ctx) + + _, status, err := readPropfind(r.Body) + if err != nil { + log.Error().Err(err).Msg("error reading propfind request") + w.WriteHeader(status) + return + } + + client, err := s.getClient() + if err != nil { + log.Error().Err(err).Msg("error getting grpc client") + w.WriteHeader(http.StatusInternalServerError) + return + } + + ref := &storageproviderv0alphapb.Reference{ + Spec: &storageproviderv0alphapb.Reference_Id{Id: rid}, + } + req := &storageproviderv0alphapb.StatRequest{Ref: ref} + res, err := client.Stat(ctx, req) + if err != nil { + log.Error().Err(err).Msg("error sending a grpc stat request") + w.WriteHeader(http.StatusInternalServerError) + return + } + if res.Status.Code != rpcpb.Code_CODE_OK { + if res.Status.Code == rpcpb.Code_CODE_NOT_FOUND { + w.WriteHeader(http.StatusNotFound) + return + } + w.WriteHeader(http.StatusInternalServerError) + return + } + + info := res.Info + + lvReq := &storageproviderv0alphapb.ListFileVersionsRequest{ + Ref: ref, + } + lvRes, err := client.ListFileVersions(ctx, lvReq) + if err != nil { + log.Error().Err(err).Msg("error sending list container grpc request") + w.WriteHeader(http.StatusInternalServerError) + return + } + if lvRes.Status.Code != rpcpb.Code_CODE_OK { + w.WriteHeader(http.StatusInternalServerError) + return + } + versions := lvRes.GetVersions() + infos := make([]*storageproviderv0alphapb.ResourceInfo, 0, len(versions)+1) + // add version dir . entry, derived from file info + infos = append(infos, &storageproviderv0alphapb.ResourceInfo{ + Type: storageproviderv0alphapb.ResourceType_RESOURCE_TYPE_CONTAINER, + Id: &storageproviderv0alphapb.ResourceId{ + StorageId: "virtual", // this is a virtual storage + OpaqueId: path.Join("meta", wrapResourceID(rid), "v"), + }, + Etag: info.Etag, + MimeType: "httpd/unix-directory", + Mtime: info.Mtime, + Path: "v", + //PermissionSet + Size: 0, + Owner: info.Owner, + }) + + for i := range versions { + vi := &storageproviderv0alphapb.ResourceInfo{ + // TODO(jfd) we cannot access version content, this will be a problem when trying to fetch version thumbnails + //Opaque + Type: storageproviderv0alphapb.ResourceType_RESOURCE_TYPE_FILE, + Id: &storageproviderv0alphapb.ResourceId{ + StorageId: "versions", // this is a virtual storage + OpaqueId: info.Id.OpaqueId + "@" + versions[i].GetKey(), + }, + //Checksum + //Etag: v.ETag, + //MimeType + Mtime: &typespb.Timestamp{ + Seconds: versions[i].Mtime, + // TODO cs3apis FileVersion should use typespb.Timestamp instead of uint64 + }, + Path: path.Join("v", versions[i].Key), + //PermissionSet + Size: versions[i].Size, + Owner: info.Owner, + } + infos = append(infos, vi) + } + + propRes, err := s.formatPropfind(ctx, infos) + if err != nil { + log.Error().Err(err).Msg("error formatting propfind") + w.WriteHeader(http.StatusInternalServerError) + return + } + w.Header().Set("DAV", "1, 3, extended-mkcol") + w.Header().Set("Content-Type", "application/xml; charset=utf-8") + w.WriteHeader(http.StatusMultiStatus) + _, err = w.Write([]byte(propRes)) + if err != nil { + log.Error().Err(err).Msg("error writing body") + return + } + +} + +func (h *VersionsHandler) doRestore(w http.ResponseWriter, r *http.Request, s *svc, rid *storageproviderv0alphapb.ResourceId, key string) { + ctx := r.Context() + log := appctx.GetLogger(ctx) + + client, err := s.getClient() + if err != nil { + log.Error().Err(err).Msg("error getting grpc client") + w.WriteHeader(http.StatusInternalServerError) + return + } + + req := &storageproviderv0alphapb.RestoreFileVersionRequest{ + Ref: &storageproviderv0alphapb.Reference{ + Spec: &storageproviderv0alphapb.Reference_Id{Id: rid}, + }, + Key: key, + } + + res, err := client.RestoreFileVersion(ctx, req) + if err != nil { + log.Error().Err(err).Msg("error sending a grpc restore version request") + w.WriteHeader(http.StatusInternalServerError) + return + } + if res.Status.Code != rpcpb.Code_CODE_OK { + if res.Status.Code == rpcpb.Code_CODE_NOT_FOUND { + w.WriteHeader(http.StatusNotFound) + return + } + w.WriteHeader(http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusNoContent) +} diff --git a/cmd/revad/svcs/httpsvcs/ocdavsvc/webdav.go b/cmd/revad/svcs/httpsvcs/ocdavsvc/webdav.go new file mode 100644 index 0000000000..30d359eb8f --- /dev/null +++ b/cmd/revad/svcs/httpsvcs/ocdavsvc/webdav.go @@ -0,0 +1,116 @@ +// Copyright 2018-2019 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 ocdavsvc + +import ( + "context" + "net/http" + "net/url" + "path" + + "github.com/cs3org/reva/pkg/appctx" + "github.com/cs3org/reva/pkg/user" +) + +// WebDavHandler routes to the legacy dav endpoint +type WebDavHandler struct { +} + +func (h *WebDavHandler) init(c *Config) error { + return nil +} + +// Handler handles requests +func (h *WebDavHandler) Handler(s *svc) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log := appctx.GetLogger(r.Context()) + + if r.Method == http.MethodOptions { + // no need for the user, and we need to be able + // to answer preflight checks, which have no auth headers + s.doOptions(w, r) + return + } + + // inject username in path + ctx := r.Context() + u, ok := user.ContextGetUser(ctx) + if !ok { + log.Error().Msg("error getting user from context") + w.WriteHeader(http.StatusInternalServerError) + return + } + // TODO(labkode): this assumes too much, basically using ocdavsvc you can't access a global namespace. + // This must be changed. + // r.URL.Path = path.Join("/", u.Username, tail2) + r.URL.Path = path.Join("/", u.Username, r.URL.Path) + + // webdav should be death: baseURI is encoded as part of the + // response payload in href field + baseURI := path.Join("/", s.Prefix(), "remote.php/webdav") + ctx = context.WithValue(r.Context(), ctxKeyBaseURI, baseURI) + + // inject username into Destination header if present + dstHeader := r.Header.Get("Destination") + if dstHeader != "" { + dstURL, err := url.ParseRequestURI(dstHeader) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + if dstURL.Path[:18] != "/remote.php/webdav" { + log.Warn().Str("path", dstURL.Path).Msg("dst needs to start with /remote.php/webdav/") + w.WriteHeader(http.StatusBadRequest) + return + } + r.Header.Set("Destination", path.Join(baseURI, u.Username, dstURL.Path[18:])) // 18 = len ("/remote.php/webdav") + } + + r = r.WithContext(ctx) + + switch r.Method { + case "PROPFIND": + s.doPropfind(w, r) + case http.MethodHead: + s.doHead(w, r) + case http.MethodGet: + s.doGet(w, r) + case "LOCK": + s.doLock(w, r) + case "UNLOCK": + s.doUnlock(w, r) + case "PROPPATCH": + s.doProppatch(w, r) + case "MKCOL": + s.doMkcol(w, r) + case "MOVE": + s.doMove(w, r) + case "COPY": + s.doCopy(w, r) + case http.MethodPut: + s.doPut(w, r) + case http.MethodDelete: + s.doDelete(w, r) + case "REPORT": + s.doReport(w, r) + default: + w.WriteHeader(http.StatusNotFound) + } + }) +} diff --git a/cmd/revad/svcs/httpsvcs/ocssvc/ocs.go b/cmd/revad/svcs/httpsvcs/ocssvc/ocs.go index 5a90f4ecfd..8b3b03dc27 100644 --- a/cmd/revad/svcs/httpsvcs/ocssvc/ocs.go +++ b/cmd/revad/svcs/httpsvcs/ocssvc/ocs.go @@ -23,6 +23,7 @@ import ( "encoding/xml" "net/http" + typespb "github.com/cs3org/go-cs3apis/cs3/types" "github.com/cs3org/reva/pkg/appctx" ) @@ -142,3 +143,14 @@ func WriteOCSResponse(w http.ResponseWriter, r *http.Request, res *Response, err w.WriteHeader(http.StatusInternalServerError) } } + +// UserIDToString returns a userid string with an optional idp separated by @: "[@]" +func UserIDToString(userID *typespb.UserId) string { + if userID == nil || userID.OpaqueId == "" { + return "" + } + if userID.Idp == "" { + return userID.OpaqueId + } + return userID.OpaqueId + "@" + userID.Idp +} diff --git a/cmd/revad/svcs/httpsvcs/ocssvc/ocssvc.go b/cmd/revad/svcs/httpsvcs/ocssvc/ocssvc.go index b7c2490f65..bfb73aec66 100644 --- a/cmd/revad/svcs/httpsvcs/ocssvc/ocssvc.go +++ b/cmd/revad/svcs/httpsvcs/ocssvc/ocssvc.go @@ -43,7 +43,6 @@ type Config struct { type svc struct { c *Config - handler http.Handler V1Handler *V1Handler } @@ -64,7 +63,6 @@ func New(m map[string]interface{}) (httpsvcs.Service, error) { return nil, err } - s.setHandler() return s, nil } @@ -72,14 +70,11 @@ func (s *svc) Prefix() string { return s.c.Prefix } -func (s *svc) Handler() http.Handler { - return s.handler -} func (s *svc) Close() error { return nil } -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 diff --git a/cmd/revad/svcs/httpsvcs/ocssvc/shares.go b/cmd/revad/svcs/httpsvcs/ocssvc/shares.go index ef65c41be3..8977aa8a68 100644 --- a/cmd/revad/svcs/httpsvcs/ocssvc/shares.go +++ b/cmd/revad/svcs/httpsvcs/ocssvc/shares.go @@ -19,6 +19,7 @@ package ocssvc import ( + "context" "encoding/json" "fmt" "net/http" @@ -67,18 +68,6 @@ func getUserManager(manager string, m map[string]map[string]interface{}) (user.M return nil, fmt.Errorf("driver %s not found for user manager", manager) } -func (h *SharesHandler) getSClient() (storageproviderv0alphapb.StorageProviderServiceClient, error) { - return pool.GetStorageProviderServiceClient(h.gatewaySvc) -} - -func (h *SharesHandler) getUClient() (usershareproviderv0alphapb.UserShareProviderServiceClient, error) { - return pool.GetUserShareProviderClient(h.gatewaySvc) -} - -func (h *SharesHandler) getPClient() (publicshareproviderv0alphapb.PublicShareProviderServiceClient, error) { - return pool.GetPublicShareProviderClient(h.gatewaySvc) -} - func (h *SharesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { log := appctx.GetLogger(r.Context()) @@ -127,7 +116,7 @@ func (h *SharesHandler) findSharees(w http.ResponseWriter, r *http.Request) { log.Debug().Int("count", len(users)).Str("search", search).Msg("users found") - matches := []*MatchData{} + matches := make([]*MatchData, 0, len(users)) for _, user := range users { match := h.userAsMatch(user) @@ -152,6 +141,8 @@ func (h *SharesHandler) userAsMatch(u *authv0alphapb.User) *MatchData { Label: u.DisplayName, Value: &MatchValueData{ ShareType: int(shareTypeUser), + // TODO(jfd) find more robust userid + // username might be ok as it is uniqe at a given point in time ShareWith: u.Username, }, } @@ -181,6 +172,20 @@ func (h *SharesHandler) createShare(w http.ResponseWriter, r *http.Request) { return } + // find recipient based on username + users, err := h.userManager.FindUsers(ctx, shareWith) + if err != nil { + WriteOCSError(w, r, MetaServerError.StatusCode, "error searching recipient", err) + return + } + var recipient *authv0alphapb.User + for _, user := range users { + if user.Username == shareWith { + recipient = user + break + } + } + // we need to prefix the path with the user id u, ok := user.ContextGetUser(ctx) if !ok { @@ -214,13 +219,13 @@ func (h *SharesHandler) createShare(w http.ResponseWriter, r *http.Request) { // map role to permissions var permissions *storageproviderv0alphapb.ResourcePermissions - permissions, err := h.role2CS3Permissions(role) + permissions, err = h.role2CS3Permissions(role) if err != nil { log.Warn().Err(err).Msg("unknown role, mapping legacy permissions") permissions = asCS3Permissions(pint, nil) } - uClient, err := h.getUClient() + uClient, err := pool.GetUserShareProviderClient(h.gatewaySvc) if err != nil { WriteOCSError(w, r, MetaServerError.StatusCode, "error getting grpc client", err) return @@ -240,7 +245,7 @@ func (h *SharesHandler) createShare(w http.ResponseWriter, r *http.Request) { }, } - sClient, err := h.getSClient() + sClient, err := pool.GetStorageProviderServiceClient(h.gatewaySvc) if err != nil { WriteOCSError(w, r, MetaServerError.StatusCode, "error getting storage grpc client", err) return @@ -274,10 +279,7 @@ func (h *SharesHandler) createShare(w http.ResponseWriter, r *http.Request) { Grant: &usershareproviderv0alphapb.ShareGrant{ Grantee: &storageproviderv0alphapb.Grantee{ Type: storageproviderv0alphapb.GranteeType_GRANTEE_TYPE_USER, - Id: &typespb.UserId{ - // Idp: TODO get from where? - OpaqueId: shareWith, - }, + Id: recipient.Id, }, Permissions: &usershareproviderv0alphapb.SharePermissions{ Permissions: permissions, @@ -297,7 +299,11 @@ func (h *SharesHandler) createShare(w http.ResponseWriter, r *http.Request) { WriteOCSError(w, r, MetaServerError.StatusCode, "grpc create share request failed", err) return } - s := h.userShare2ShareData(res.Share) + s, err := h.userShare2ShareData(ctx, res.Share) + if err != nil { + WriteOCSError(w, r, MetaServerError.StatusCode, "error mapping share data", err) + return + } s.Path = r.FormValue("path") // use path without user prefix WriteOCSSuccess(w, r, s) return @@ -464,7 +470,7 @@ func (h *SharesHandler) updateShare(w http.ResponseWriter, r *http.Request) { shareID := strings.TrimLeft(r.URL.Path, "/") // TODO we need to lookup the storage that is responsible for this share - uClient, err := h.getUClient() + uClient, err := pool.GetUserShareProviderClient(h.gatewaySvc) if err != nil { WriteOCSError(w, r, MetaServerError.StatusCode, "error getting grpc client", err) return @@ -526,7 +532,11 @@ func (h *SharesHandler) updateShare(w http.ResponseWriter, r *http.Request) { return } - share := h.userShare2ShareData(gRes.Share) + share, err := h.userShare2ShareData(ctx, gRes.Share) + if err != nil { + WriteOCSError(w, r, MetaServerError.StatusCode, "error mapping share data", err) + return + } WriteOCSSuccess(w, r, share) } @@ -553,7 +563,7 @@ func (h *SharesHandler) listShares(w http.ResponseWriter, r *http.Request) { log.Debug().Str("path", p).Str("fn", fn).Interface("user", u).Msg("resolved path for user") // first check if the file exists - sClient, err := h.getSClient() + sClient, err := pool.GetStorageProviderServiceClient(h.gatewaySvc) if err != nil { WriteOCSError(w, r, MetaServerError.StatusCode, "error getting grpc storage provider client", err) return @@ -585,9 +595,7 @@ func (h *SharesHandler) listShares(w http.ResponseWriter, r *http.Request) { filters = append(filters, &usershareproviderv0alphapb.ListSharesRequest_Filter{ Type: usershareproviderv0alphapb.ListSharesRequest_Filter_LIST_SHARES_REQUEST_FILTER_TYPE_RESOURCE_ID, Term: &usershareproviderv0alphapb.ListSharesRequest_Filter_ResourceId{ - // TODO the usershareprovider currently expects a path as the opacque id. It must accept proper ResourceIDs - // ResourceId: info.Id, - ResourceId: &storageproviderv0alphapb.ResourceId{StorageId: info.Id.StorageId, OpaqueId: fn}, + ResourceId: info.Id, }, }) } @@ -596,7 +604,7 @@ func (h *SharesHandler) listShares(w http.ResponseWriter, r *http.Request) { // fetch user shares if configured if h.gatewaySvc != "" { - uClient, err := h.getUClient() + uClient, err := pool.GetUserShareProviderClient(h.gatewaySvc) if err != nil { WriteOCSError(w, r, MetaServerError.StatusCode, "error getting grpc user share handler client", err) return @@ -618,8 +626,15 @@ func (h *SharesHandler) listShares(w http.ResponseWriter, r *http.Request) { return } for _, s := range res.Shares { - share := h.userShare2ShareData(s) - h.addFileInfo(share, info) + share, err := h.userShare2ShareData(ctx, s) + if err != nil { + WriteOCSError(w, r, MetaServerError.StatusCode, "error mapping share data", err) + return + } + if h.addFileInfo(ctx, share, info) != nil { + WriteOCSError(w, r, MetaServerError.StatusCode, "error adding file info", err) + return + } log.Debug().Interface("share", s).Interface("info", info).Interface("shareData", share).Msg("mapped") shares = append(shares, share) } @@ -628,12 +643,11 @@ func (h *SharesHandler) listShares(w http.ResponseWriter, r *http.Request) { // TODO fetch federated shares // fetch public link shares if configured - if h.gatewaySvc != "" { - pClient, err := h.getPClient() - if err != nil { - WriteOCSError(w, r, MetaServerError.StatusCode, "error getting grpc public share provider client", err) - return - } + pClient, err := pool.GetPublicShareProviderClient(h.gatewaySvc) + if err != nil { + // TODO(jfd) log error if it is not available, log nothing if disabled ... somehow + log.Error().Err(err).Msg("error getting grpc public share provider client") + } else { req := &publicshareproviderv0alphapb.ListPublicSharesRequest{} res, err := pClient.ListPublicShares(ctx, req) if err != nil { @@ -650,7 +664,11 @@ func (h *SharesHandler) listShares(w http.ResponseWriter, r *http.Request) { } for _, s := range res.Share { share := h.publicShare2ShareData(s) - h.addFileInfo(share, info) + err := h.addFileInfo(ctx, share, info) + if err != nil { + WriteOCSError(w, r, MetaServerError.StatusCode, "error adding file info", err) + return + } log.Debug().Interface("share", s).Interface("info", info).Interface("shareData", share).Msg("mapped") shares = append(shares, share) } @@ -661,10 +679,9 @@ func (h *SharesHandler) listShares(w http.ResponseWriter, r *http.Request) { }) } -func (h *SharesHandler) addFileInfo(s *ShareData, info *storageproviderv0alphapb.ResourceInfo) { +func (h *SharesHandler) addFileInfo(ctx context.Context, s *ShareData, info *storageproviderv0alphapb.ResourceInfo) error { if info != nil { // TODO The owner is not set in the storage stat metadata ... - owner := h.resolveUserID(info.Owner) s.MimeType = info.MimeType // TODO STime: &typespb.Timestamp{Seconds: info.Mtime.Seconds, Nanos: info.Mtime.Nanos}, s.StorageID = info.Id.StorageId @@ -677,35 +694,60 @@ func (h *SharesHandler) addFileInfo(s *ShareData, info *storageproviderv0alphapb // file owner might not yet be set. Use file info if s.UIDFileOwner == "" { - s.UIDFileOwner = owner.Id.OpaqueId + s.UIDFileOwner = UserIDToString(info.Owner) } - if s.DisplaynameFileOwner == "" { + if s.DisplaynameFileOwner == "" && info.Owner != nil { + owner, err := h.userManager.GetUser(ctx, info.Owner) + if err != nil { + return err + } s.DisplaynameFileOwner = owner.DisplayName } // share owner might not yet be set. Use file info if s.UIDOwner == "" { - s.UIDOwner = owner.Id.OpaqueId + s.UIDOwner = UserIDToString(info.Owner) } - if s.DisplaynameOwner == "" { + if s.DisplaynameOwner == "" && info.Owner != nil { + owner, err := h.userManager.GetUser(ctx, info.Owner) + if err != nil { + return err + } s.DisplaynameOwner = owner.DisplayName } } + return nil } // TODO(jfd) merge userShare2ShareData with publicShare2ShareData -func (h *SharesHandler) userShare2ShareData(share *usershareproviderv0alphapb.Share) *ShareData { - creator := h.resolveUserID(share.Creator) - owner := h.resolveUserID(share.Owner) - grantee := h.resolveUserID(share.Grantee.Id) +func (h *SharesHandler) userShare2ShareData(ctx context.Context, share *usershareproviderv0alphapb.Share) (*ShareData, error) { sd := &ShareData{ - Permissions: userSharePermissions2OCSPermissions(share.GetPermissions()), - ShareType: shareTypeUser, - UIDOwner: creator.Id.OpaqueId, - DisplaynameOwner: creator.DisplayName, - UIDFileOwner: owner.Id.OpaqueId, - DisplaynameFileOwner: owner.DisplayName, - ShareWith: grantee.Id.OpaqueId, - ShareWithDisplayname: grantee.DisplayName, + Permissions: userSharePermissions2OCSPermissions(share.GetPermissions()), + ShareType: shareTypeUser, + } + if share.Creator != nil { + if creator, err := h.userManager.GetUser(ctx, share.Creator); err == nil { + // TODO the user from GetUser might not have an ID set, so we are using the one we have + sd.UIDOwner = UserIDToString(share.Creator) + sd.DisplaynameOwner = creator.DisplayName + } else { + return nil, err + } + } + if share.Owner != nil { + if owner, err := h.userManager.GetUser(ctx, share.Owner); err == nil { + sd.UIDFileOwner = UserIDToString(share.Owner) + sd.DisplaynameFileOwner = owner.DisplayName + } else { + return nil, err + } + } + if share.Grantee.Id != nil { + if grantee, err := h.userManager.GetUser(ctx, share.Grantee.Id); err == nil { + sd.ShareWith = UserIDToString(share.Grantee.Id) + sd.ShareWithDisplayname = grantee.DisplayName + } else { + return nil, err + } } if share.Id != nil && share.Id.OpaqueId != "" { sd.ID = share.Id.OpaqueId @@ -715,7 +757,7 @@ func (h *SharesHandler) userShare2ShareData(share *usershareproviderv0alphapb.Sh } // actually clients should be able to GET and cache the user info themselves ... // TODO check grantee type for user vs group - return sd + return sd, nil } func userSharePermissions2OCSPermissions(sp *usershareproviderv0alphapb.SharePermissions) Permissions { @@ -732,34 +774,22 @@ func publicSharePermissions2OCSPermissions(sp *publicshareproviderv0alphapb.Publ return permissionInvalid } -// TODO do user lookup and cache users -func (h *SharesHandler) resolveUserID(userID *typespb.UserId) *authv0alphapb.User { - return &authv0alphapb.User{ - Id: &typespb.UserId{ - Idp: userID.Idp, - OpaqueId: userID.OpaqueId, - }, - Username: userID.OpaqueId, - DisplayName: userID.OpaqueId, - } -} - func (h *SharesHandler) publicShare2ShareData(share *publicshareproviderv0alphapb.PublicShare) *ShareData { - creator := h.resolveUserID(share.Creator) - owner := h.resolveUserID(share.Owner) sd := &ShareData{ ID: share.Id.OpaqueId, // TODO map share.resourceId to path and storage ... requires a stat call // share.permissions ar mapped below - Permissions: publicSharePermissions2OCSPermissions(share.GetPermissions()), - ShareType: shareTypePublicLink, - UIDOwner: creator.Id.OpaqueId, - DisplaynameOwner: creator.DisplayName, - STime: share.Ctime.Seconds, // TODO CS3 api birth time = btime - UIDFileOwner: owner.Id.OpaqueId, - DisplaynameFileOwner: owner.DisplayName, - Token: share.Token, - Expiration: timestampToExpiration(share.Expiration), + Permissions: publicSharePermissions2OCSPermissions(share.GetPermissions()), + ShareType: shareTypePublicLink, + UIDOwner: UserIDToString(share.Creator), + // TODO lookup user metadata + //DisplaynameOwner: creator.DisplayName, + STime: share.Ctime.Seconds, // TODO CS3 api birth time = btime + UIDFileOwner: UserIDToString(share.Owner), + // TODO lookup user metadata + //DisplaynameFileOwner: owner.DisplayName, + Token: share.Token, + Expiration: timestampToExpiration(share.Expiration), } // actually clients should be able to GET and cache the user info themselves ... // TODO check grantee type for user vs group diff --git a/cmd/revad/users.demo.json b/cmd/revad/users.demo.json index b53626d557..99bd81dfc1 100644 --- a/cmd/revad/users.demo.json +++ b/cmd/revad/users.demo.json @@ -1,26 +1,32 @@ [ { - "sub": "37a08ed30093a133b1bb4ae0b8f3601f", - "iss": "localhost:9998", + "id": { + "opaque_id": "37a08ed30093a133b1bb4ae0b8f3601f", + "idp": "localhost" + }, "username": "einstein", "secret": "relativity", "mail": "einstein@example.org", - "displayname": "Albert Einstein" + "display_name": "Albert Einstein" }, { - "sub": "b3725122c9d3bfef5664619e08e31877", - "iss": "localhost:9998", + "id": { + "opaque_id": "b3725122c9d3bfef5664619e08e31877", + "idp": "localhost" + }, "username": "marie", "secret": "radioactivity", "mail": "marie@example.org", - "displayname": "Marie Curie" + "display_name": "Marie Curie" }, { - "sub": "6ae199a93c381bf6d5de27491139d3f9", - "iss": "localhost:9998", + "id": { + "opaque_id": "6ae199a93c381bf6d5de27491139d3f9", + "idp": "localhost" + }, "username": "richard", "secret": "superfluidity", "mail": "richard@example.org", - "displayname": "Richard Feynman" + "display_name": "Richard Feynman" } ] diff --git a/cmd/revad/users.json b/cmd/revad/users.json index 1b05f71a99..ebed6e99f7 100644 --- a/cmd/revad/users.json +++ b/cmd/revad/users.json @@ -1,10 +1,12 @@ [ { - "sub": "a48cb0ed-3375-4b94-bdcd-3cac6e201b7b", - "iss": "example.com", + "id": { + "opaque_id": "a48cb0ed-3375-4b94-bdcd-3cac6e201b7b", + "idp": "example.com" + }, "username": "admin", "secret": "secret", "mail": "admin@example.com", - "displayname": "Admin" + "display_name": "Admin" } ] diff --git a/cmd/revad/users.oidc.json b/cmd/revad/users.oidc.json index 81ced82a46..27c1ff0f2b 100644 --- a/cmd/revad/users.oidc.json +++ b/cmd/revad/users.oidc.json @@ -1,26 +1,32 @@ [ { - "sub": "c6e5995d6c7fa1986b830b78b478e6c2", - "iss": "localhost:9998", + "id": { + "opaque_id": "c6e5995d6c7fa1986b830b78b478e6c2", + "idp": "localhost:9998" + }, "username": "aaliyah_abernathy", "secret": "secret", "mail": "aaliyah_abernathy@owncloudqa.com", - "displayname": "Aaliyah Abernathy" + "display_name": "Aaliyah Abernathy" }, { - "sub": "9fb5f8d212cbf3fc55f1bf67d97ed05d", - "iss": "localhost:9998", + "id": { + "opaque_id": "9fb5f8d212cbf3fc55f1bf67d97ed05d", + "idp": "localhost:9998" + }, "username": "aaliyah_adams", "secret": "secret", "mail": "aaliyah_adams@owncloudqa.com", - "displayname": "Aaliyah Adams" + "display_name": "Aaliyah Adams" }, { - "sub": "a84075b398fe6a0aee1155f8ead13331", - "iss": "localhost:9998", + "id": { + "opaque_id": "a84075b398fe6a0aee1155f8ead13331", + "idp": "localhost:9998" + }, "username": "aaliyah_anderson", "secret": "secret", "mail": "aaliyah_anderson@owncloudqa.com", - "displayname": "Aaliyah Anderson" + "display_name": "Aaliyah Anderson" } ] diff --git a/go.mod b/go.mod index 60e8b1d137..301998fff8 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/go-openapi/strfmt v0.19.2 // indirect github.com/gofrs/uuid v3.2.0+incompatible github.com/gogo/protobuf v1.2.0 // indirect + github.com/gomodule/redigo v2.0.0+incompatible github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4 github.com/jedib0t/go-pretty v4.3.0+incompatible github.com/mattn/go-colorable v0.0.9 // indirect @@ -21,12 +22,14 @@ require ( github.com/mitchellh/mapstructure v1.1.2 github.com/ory/fosite v0.29.7 github.com/pkg/errors v0.8.1 + github.com/pkg/xattr v0.4.1 github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829 // indirect github.com/rs/cors v1.7.0 github.com/rs/zerolog v1.15.0 go.opencensus.io v0.22.0 golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a + golang.org/x/net v0.0.0-20190620200207-3b0461eec859 // indirect golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 google.golang.org/grpc v1.23.0 gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect diff --git a/go.sum b/go.sum index 756a0492c2..f50379973d 100644 --- a/go.sum +++ b/go.sum @@ -8,7 +8,9 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/apache/thrift v0.12.0 h1:pODnxUFNcjP9UTLZGTdeh+j16A8lJbRvD3rOtrk/7bs= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= @@ -61,6 +63,8 @@ github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= +github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -120,6 +124,8 @@ github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/xattr v0.4.1 h1:dhclzL6EqOXNaPDWqoeb9tIxATfBSmjqL0b4DpSjwRw= +github.com/pkg/xattr v0.4.1/go.mod h1:W2cGD0TBEus7MkUgv0tNZ9JutLtVO3cXu+IBRuHqnFs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU= @@ -144,6 +150,7 @@ github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.15.0 h1:uPRuwkWF4J6fGsJ2R0Gn2jB1EQiav9k3S6CSdygQJXY= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= @@ -186,6 +193,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09 h1:KaQtG+aDELoNmXYas3TVkGNYRuq8JQ1aa7LJt8EXVyo= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181003184128-c57b0facaced h1:4oqSq7eft7MdPKBGQK11X9WYUxmj6ZLgGTqYIbY1kyw= @@ -201,6 +210,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181021155630-eda9bb28ed51/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -241,6 +251,7 @@ google.golang.org/grpc v1.22.0 h1:J0UbZOIrCAl+fpTOf8YLs4dJo8L/owV4LYVtAXQoPkw= google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0 h1:AzbTB6ux+okLTzP8Ru1Xs41C303zdcfEht7MQnYJt5A= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d h1:TxyelI5cVkbREznMhfzycHdkp5cLA7DpE+GKjSslYhM= gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= diff --git a/pkg/storage/fs/loader/loader.go b/pkg/storage/fs/loader/loader.go index 5e289ba780..83733c3cbe 100644 --- a/pkg/storage/fs/loader/loader.go +++ b/pkg/storage/fs/loader/loader.go @@ -22,6 +22,7 @@ import ( // Load core storage filesystem backends. _ "github.com/cs3org/reva/pkg/storage/fs/eos" _ "github.com/cs3org/reva/pkg/storage/fs/local" + _ "github.com/cs3org/reva/pkg/storage/fs/owncloud" _ "github.com/cs3org/reva/pkg/storage/fs/s3" // Add your own here ) diff --git a/pkg/storage/fs/owncloud/owncloud.go b/pkg/storage/fs/owncloud/owncloud.go new file mode 100644 index 0000000000..6b3a2407d6 --- /dev/null +++ b/pkg/storage/fs/owncloud/owncloud.go @@ -0,0 +1,1130 @@ +// Copyright 2018-2019 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 owncloud + +import ( + "context" + "crypto/md5" + "encoding/binary" + "encoding/csv" + "fmt" + "io" + "io/ioutil" + "os" + "path" + "path/filepath" + "strconv" + "strings" + "syscall" + "time" + + storageproviderv0alphapb "github.com/cs3org/go-cs3apis/cs3/storageprovider/v0alpha" + typespb "github.com/cs3org/go-cs3apis/cs3/types" + "github.com/cs3org/reva/pkg/appctx" + "github.com/cs3org/reva/pkg/errtypes" + "github.com/cs3org/reva/pkg/logger" + "github.com/cs3org/reva/pkg/mime" + "github.com/cs3org/reva/pkg/storage" + "github.com/cs3org/reva/pkg/storage/fs/registry" + "github.com/gofrs/uuid" + "github.com/gomodule/redigo/redis" + "github.com/mitchellh/mapstructure" + "github.com/pkg/errors" + "github.com/pkg/xattr" +) + +const ( + // Currently,extended file attributes have four separated + // namespaces (user, trusted, security and system) followed by a dot. + // A non ruut user can only manipulate the user. namespace, which is what + // we will use to store ownCloud specific metadata. To prevent name + // collisions with other apps We are going to introduca a sub namespace + // "user.oc." + + // idAttribute is the name of the filesystem extended attribute that is used to store the uuid in + idAttribute string = "user.oc.id" + + // shares are persisted using extended attributes. We are going to mimic + // NFS4 ACLs, with one extended attribute per share, following Access + // Control Entries (ACEs). The following is taken from the nfs4_acl man page, + // see https://linux.die.net/man/5/nfs4_acl: + // the extended attributes will look like this + // "user.oc.acl.:::" + // - *type* will be limited to A for now + // A: Allow - allow *principal* to perform actions requiring *permissions* + // In the future we can use: + // U: aUdit - log any attempted access by principal which requires + // permissions. + // L: aLarm - generate a system alarm at any attempted access by + // principal which requires permissions + // D for deny is not recommended + // - *flags* for now empty or g for group, no inheritance yet + // - d directory-inherit - newly-created subdirectories will inherit the + // ACE. + // - f file-inherit - newly-created files will inherit the ACE, minus its + // inheritance flags. Newly-created subdirectories + // will inherit the ACE; if directory-inherit is not + // also specified in the parent ACE, inherit-only will + // be added to the inherited ACE. + // - n no-propagate-inherit - newly-created subdirectories will inherit + // the ACE, minus its inheritance flags. + // - i inherit-only - the ACE is not considered in permissions checks, + // but it is heritable; however, the inherit-only + // flag is stripped from inherited ACEs. + // - *principal* a named user, group or special principal + // - the oidc sub@iss maps nicely to this + // - 'OWNER@', 'GROUP@', and 'EVERYONE@', which are, respectively, analogous to the POSIX user/group/other + // - *permissions* + // - r read-data (files) / list-directory (directories) + // - w write-data (files) / create-file (directories) + // - a append-data (files) / create-subdirectory (directories) + // - x execute (files) / change-directory (directories) + // - d delete - delete the file/directory. Some servers will allow a delete to occur if either this permission is set in the file/directory or if the delete-child permission is set in its parent direcory. + // - D delete-child - remove a file or subdirectory from within the given directory (directories only) + // - t read-attributes - read the attributes of the file/directory. + // - T write-attributes - write the attributes of the file/directory. + // - n read-named-attributes - read the named attributes of the file/directory. + // - N write-named-attributes - write the named attributes of the file/directory. + // - c read-ACL - read the file/directory NFSv4 ACL. + // - C write-ACL - write the file/directory NFSv4 ACL. + // - o write-owner - change ownership of the file/directory. + // - y synchronize - allow clients to use synchronous I/O with the server. + // TODO implement OWNER@ as "user.oc.acl.A::OWNER@:rwaDxtTnNcCy" + // attribute names are limited to 255 chars by the linux kernel vfs, values to 64 kb + // ext3 extended attributes must fit inside a signle filesystem block ... 4096 bytes + // that leaves us with "user.oc.acl.A::someonewithaslightlylongersubject@whateverissuer:rwaDxtTnNcCy" ~80 chars + // 4096/80 = 51 shares ... with luck we might move the actual permissions to the value, saving ~15 chars + // 4096/64 = 64 shares ... still meh ... we can do better by using ints instead of strings for principals + // "user.oc.acl.u:100000" is pretty neat, but we can still do better: base64 encode the int + // "user.oc.acl.u:6Jqg" but base64 always has at least 4 chars, maybe hex is better for smaller numbers + // well use 4 chars in addition to the ace: "user.oc.acl.u:////" = 65535 -> 18 chars + // 4096/18 = 227 shares + // still ... ext attrs for this are not infinite scale ... + // so .. attach shares via fileid. + // /metadata//shares, similar to /files + // /metadata//shares/u///A:fdi:rwaDxtTnNcCy permissions as filename to keep them in the stat cache? + // + // whatever ... 50 shares is good enough. If more is needed we can delegate to the metadata + // if "user.oc.acl.M" is present look inside the metadata app. + // - if we cannot set an ace we might get an io error. + // in that case convert all shares to metadata and try to set "user.oc.acl.m" + // + // what about metadata like share creator, share time, expiry? + // - creator is same as owner, but can be set + // - share date, or abbreviated st is a unix timestamp + // - expiry is a unix timestamp + // - can be put inside the value + // - we need to reorder the fields: + // "user.oc.acl.:" -> "kv:t=:f=:p=:st=:c=:e=:pw=:n=" + // "user.oc.acl.:" -> "v1::::::::" + // or the first byte determines the format + // 0x00 = key value + // 0x01 = v1 ... + // + // SharePrefix is the prefix for sharing related extended attributes + sharePrefix string = "user.oc.acl." +) + +func init() { + registry.Register("owncloud", New) +} + +type config struct { + DataDirectory string `mapstructure:"datadirectory"` + Scan bool `mapstructure:"scan"` + Autocreate bool `mapstructure:"autocreate"` + Redis string `mapstructure:"redis"` +} + +func parseConfig(m map[string]interface{}) (*config, error) { + c := &config{} + if err := mapstructure.Decode(m, c); err != nil { + err = errors.Wrap(err, "error decoding conf") + return nil, err + } + return c, nil +} + +func (c *config) init(m map[string]interface{}) { + if c.Redis == "" { + c.Redis = ":6379" + } + // default to scanning if not configured + if _, ok := m["scan"]; !ok { + c.Scan = true + } + // default to autocreate if not configured + if _, ok := m["scan"]; !ok { + c.Autocreate = true + } +} + +// New returns an implementation to of the storage.FS interface that talk to +// a local filesystem. +func New(m map[string]interface{}) (storage.FS, error) { + c, err := parseConfig(m) + if err != nil { + return nil, err + } + c.init(m) + + // c.DataDirectoryshould never end in / unless it is the root? + c.DataDirectory = path.Clean(c.DataDirectory) + + // create datadir if it does not exist + err = os.MkdirAll(c.DataDirectory, 0700) + if err != nil { + logger.New().Error().Err(err). + Str("path", c.DataDirectory). + Msg("could not create datadir") + } + + pool := &redis.Pool{ + + MaxIdle: 3, + IdleTimeout: 240 * time.Second, + + Dial: func() (redis.Conn, error) { + c, err := redis.Dial("tcp", c.Redis) + if err != nil { + return nil, err + } + return c, err + }, + + TestOnBorrow: func(c redis.Conn, t time.Time) error { + _, err := c.Do("PING") + return err + }, + } + + return &ocFS{c: c, pool: pool}, nil +} + +type ocFS struct { + c *config + pool *redis.Pool +} + +func (fs *ocFS) Shutdown(ctx context.Context) error { + return fs.pool.Close() +} + +// scan files and add uuid to path mapping to kv store +func (fs *ocFS) scanFiles(ctx context.Context, conn redis.Conn) { + if fs.c.Scan { + fs.c.Scan = false // TODO ... in progress use mutex ? + log := appctx.GetLogger(ctx) + log.Debug().Str("path", fs.c.DataDirectory).Msg("scanning data directory") + err := filepath.Walk(fs.c.DataDirectory, func(path string, info os.FileInfo, err error) error { + if err != nil { + log.Error().Str("path", path).Err(err).Msg("error accessing path") + return filepath.SkipDir + } + // TODO(jfd) skip versions folder only if direct in users home dir + // we need to skip versions, otherwise a lookup by id might resolve to a version + if strings.Contains(path, "files_versions") { + log.Debug().Str("path", path).Err(err).Msg("skipping versions") + return filepath.SkipDir + } + + // reuse connection to store file ids + id := readOrCreateID(context.Background(), path, nil) + _, err = conn.Do("SET", id, path) + if err != nil { + log.Error().Str("path", path).Err(err).Msg("error caching id") + // continue scanning + return nil + } + + log.Debug().Str("path", path).Str("id", id).Msg("scanned path") + return nil + }) + if err != nil { + log.Error().Err(err).Str("path", fs.c.DataDirectory).Msg("error scanning data directory") + } + } +} + +// ownloud stores files in the files subfolder +// the incoming path starts with /, so we need to insert the files subfolder into the path +// and prefix the datadirectory +// TODO the path handed to a storage provider should not contain the username +func (fs *ocFS) getInternalPath(ctx context.Context, fn string) string { + // p = / or + // p = //foo/bar.txt + parts := strings.SplitN(fn, "/", 3) + + switch len(parts) { + case 2: + // parts = "", "" + return path.Join(fs.c.DataDirectory, parts[1], "files") + case 3: + // parts = "", "", "foo/bar.txt" + return path.Join(fs.c.DataDirectory, parts[1], "files", parts[2]) + default: + return "" // TODO Must not happen? + } +} + +// ownloud stores versions in the files_versions subfolder +// the incoming path starts with /, so we need to insert the files subfolder into the path +// and prefix the datadirectory +// TODO the path handed to a storage provider should not contain the username +func (fs *ocFS) getVersionsPath(ctx context.Context, np string) string { + // np = /path/to/data//files/foo/bar.txt + // remove data dir + if fs.c.DataDirectory != "/" { + // fs.c.DataDirectory is a clean puth, so it never ends in / + np = strings.TrimPrefix(np, fs.c.DataDirectory) + } + // np = //files/foo/bar.txt + parts := strings.SplitN(np, "/", 4) + + switch len(parts) { + case 3: + // parts = "", "" + return path.Join(fs.c.DataDirectory, parts[1], "files_versions") + case 4: + // parts = "", "", "foo/bar.txt" + return path.Join(fs.c.DataDirectory, parts[1], "files_versions", parts[3]) + default: + return "" // TODO Must not happen? + } + +} + +func (fs *ocFS) removeNamespace(ctx context.Context, np string) string { + // np = /data//files/foo/bar.txt + // remove data dir + if fs.c.DataDirectory != "/" { + // fs.c.DataDirectory is a clean puth, so it never ends in / + np = strings.TrimPrefix(np, fs.c.DataDirectory) + // np = //files/foo/bar.txt + } + + parts := strings.SplitN(np, "/", 4) + // parts = "", "", "files", "foo/bar.txt" + switch len(parts) { + case 1: + return "/" + case 2: + return path.Join("/", parts[1]) + case 3: + return path.Join("/", parts[1]) + default: + return path.Join("/", parts[1], parts[3]) + } +} + +// calcEtag will create an etag based on the md5 of +// - mtime, +// - inode (if available), +// - device (if available) and +// - size. +// errors are logged, but an etag will still be returned +func calcEtag(ctx context.Context, fi os.FileInfo) string { + log := appctx.GetLogger(ctx) + h := md5.New() + err := binary.Write(h, binary.BigEndian, fi.ModTime().Unix()) + if err != nil { + log.Error().Err(err).Msg("error writing mtime") + } + stat, ok := fi.Sys().(*syscall.Stat_t) + if ok { + // take device and inode into account + err = binary.Write(h, binary.BigEndian, stat.Ino) + if err != nil { + log.Error().Err(err).Msg("error writing inode") + } + err = binary.Write(h, binary.BigEndian, stat.Dev) + if err != nil { + log.Error().Err(err).Msg("error writing device") + } + } + err = binary.Write(h, binary.BigEndian, fi.Size()) + if err != nil { + log.Error().Err(err).Msg("error writing size") + } + return fmt.Sprintf(`"%x"`, h.Sum(nil)) +} + +func getOwner(fn string) string { + parts := strings.SplitN(fn, "/", 3) + // parts = "", "", "files", "foo/bar.txt" + if len(parts) > 1 { + return parts[1] + } + return "" +} + +func (fs *ocFS) convertToResourceInfo(ctx context.Context, fi os.FileInfo, np string, c redis.Conn) *storageproviderv0alphapb.ResourceInfo { + id := readOrCreateID(ctx, np, c) + fn := fs.removeNamespace(ctx, path.Join("/", np)) + + return &storageproviderv0alphapb.ResourceInfo{ + Id: &storageproviderv0alphapb.ResourceId{OpaqueId: id}, + Path: fn, + Owner: &typespb.UserId{OpaqueId: getOwner(fn)}, + Type: getResourceType(fi.IsDir()), + Etag: calcEtag(ctx, fi), + MimeType: mime.Detect(fi.IsDir(), fn), + Size: uint64(fi.Size()), + PermissionSet: &storageproviderv0alphapb.ResourcePermissions{ListContainer: true, CreateContainer: true}, + Mtime: &typespb.Timestamp{ + Seconds: uint64(fi.ModTime().Unix()), + // TODO read nanos from where? Nanos: fi.MTimeNanos, + }, + } +} +func getResourceType(isDir bool) storageproviderv0alphapb.ResourceType { + if isDir { + return storageproviderv0alphapb.ResourceType_RESOURCE_TYPE_CONTAINER + } + return storageproviderv0alphapb.ResourceType_RESOURCE_TYPE_FILE +} + +func readOrCreateID(ctx context.Context, np string, conn redis.Conn) string { + log := appctx.GetLogger(ctx) + + // read extended file attribute for id + //generate if not present + var id []byte + var err error + if id, err = xattr.Get(np, idAttribute); err != nil { + log.Warn().Err(err).Msg("error reading file id") + // try generating a uuid + if uuid, err := uuid.NewV4(); err != nil { + log.Error().Err(err).Msg("error generating fileid") + } else { + // store uuid + id = uuid.Bytes() + if err := xattr.Set(np, idAttribute, id); err != nil { + log.Error().Err(err).Msg("error storing file id") + } + // TODO cache path for uuid in redis + // TODO reuse conn? + if conn != nil { + _, err := conn.Do("SET", uuid.String(), np) + if err != nil { + log.Error().Str("path", np).Err(err).Msg("error caching id") + // continue + } + } + } + } + // todo sign metadata + var uid uuid.UUID + if uid, err = uuid.FromBytes(id); err != nil { + log.Error().Err(err).Msg("error parsing uuid") + return "" + } + return uid.String() +} + +func (fs *ocFS) autocreate(ctx context.Context, fsfn string) { + if fs.c.Autocreate { + parts := strings.SplitN(fsfn, "/files", 2) + switch len(parts) { + case 1: + return // error? there is no files in here ... + case 2: + if parts[1] == "" { + // nothing to do, fsfn is the home + } else { + // only create home + fsfn = path.Join(parts[0], "files") + } + err := os.MkdirAll(fsfn, 0700) + if err != nil { + appctx.GetLogger(ctx).Debug().Err(err). + Str("fsfn", fsfn). + Msg("could not autocreate dir") + } + } + } +} + +func (fs *ocFS) getPath(ctx context.Context, id *storageproviderv0alphapb.ResourceId) (string, error) { + c := fs.pool.Get() + defer c.Close() + fs.scanFiles(ctx, c) + np, err := redis.String(c.Do("GET", id.OpaqueId)) + if err != nil { + appctx.GetLogger(ctx).Error().Err(err).Interface("id", id).Msg("error looking up fileid") + return "", err + } + return np, nil +} + +// GetPathByID returns the fn pointed by the file id, without the internal namespace +func (fs *ocFS) GetPathByID(ctx context.Context, id *storageproviderv0alphapb.ResourceId) (string, error) { + np, err := fs.getPath(ctx, id) + if err != nil { + return "", err + } + return fs.removeNamespace(ctx, np), nil +} + +// resolve takes in a request path or request id and converts it to a internal path. +func (fs *ocFS) resolve(ctx context.Context, ref *storageproviderv0alphapb.Reference) (string, error) { + if ref.GetPath() != "" { + return fs.getInternalPath(ctx, ref.GetPath()), nil + } + + if ref.GetId() != nil { + np, err := fs.getPath(ctx, ref.GetId()) + if err != nil { + return "", err + } + return np, nil + } + + // reference is invalid + return "", fmt.Errorf("invalid reference %+v", ref) +} + +func (fs *ocFS) AddGrant(ctx context.Context, ref *storageproviderv0alphapb.Reference, g *storageproviderv0alphapb.Grant) error { + np, err := fs.resolve(ctx, ref) + if err != nil { + return errors.Wrap(err, "ocFS: error resolving reference") + } + + e, err := fs.getACE(g) + if err != nil { + return err + } + + var attr string + if g.Grantee.Type == storageproviderv0alphapb.GranteeType_GRANTEE_TYPE_GROUP { + attr = sharePrefix + "g:" + e.Principal + } else { + attr = sharePrefix + "u:" + e.Principal + } + + return xattr.Set(np, attr, getValue(e)) +} + +func getValue(e *ace) []byte { + // first byte will be replaced after converting to byte array + val := fmt.Sprintf("_t=%s:f=%s:p=%s", e.Type, e.Flags, e.Permissions) + b := []byte(val) + b[0] = 0 // indicalte key value + return b +} + +func getACEPerm(set *storageproviderv0alphapb.ResourcePermissions) (string, error) { + var b strings.Builder + + if set.Stat || set.InitiateFileDownload || set.ListContainer { + b.WriteString("r") + } + if set.InitiateFileUpload || set.Move { + b.WriteString("w") + } + if set.CreateContainer { + b.WriteString("a") + } + if set.Delete { + b.WriteString("d") + } + + // sharing + if set.AddGrant || set.RemoveGrant || set.UpdateGrant { + b.WriteString("C") + } + if set.ListGrants { + b.WriteString("c") + } + + // trash + if set.ListRecycle { + b.WriteString("u") + } + if set.RestoreRecycleItem { + b.WriteString("U") + } + if set.PurgeRecycle { + b.WriteString("P") + } + + // versions + if set.ListFileVersions { + b.WriteString("v") + } + if set.RestoreFileVersion { + b.WriteString("V") + } + + // quota + if set.GetQuota { + b.WriteString("q") + } + // TODO set quota permission? + // TODO GetPath + return b.String(), nil +} + +func (fs *ocFS) getACE(g *storageproviderv0alphapb.Grant) (*ace, error) { + permissions, err := getACEPerm(g.Permissions) + if err != nil { + return nil, err + } + e := &ace{ + Principal: g.Grantee.Id.OpaqueId, + Permissions: permissions, + // TODO creator ... + Type: "A", + } + if g.Grantee.Type == storageproviderv0alphapb.GranteeType_GRANTEE_TYPE_GROUP { + e.Flags = "g" + } + return e, nil +} + +type ace struct { + //NFSv4 acls + Type string // t + Flags string // f + Principal string // im key + Permissions string // p + + // sharing specific + ShareTime int // s + Creator string // c + Expires int // e + Password string // w passWord TODO h = hash + Label string // l +} + +func unmarshalACE(v []byte) (*ace, error) { + // first byte indicates type of value + switch v[0] { + case 0: // = ':' separated key=value pairs + s := string(v[1:]) + return unmarshalKV(s) + default: + return nil, fmt.Errorf("unknown ace encoding") + } +} + +func unmarshalKV(s string) (*ace, error) { + e := &ace{} + r := csv.NewReader(strings.NewReader(s)) + r.Comma = ':' + r.Comment = 0 + r.FieldsPerRecord = -1 + r.LazyQuotes = false + r.TrimLeadingSpace = false + records, err := r.ReadAll() + if err != nil { + return nil, err + } + if len(records) != 1 { + return nil, fmt.Errorf("more than one row of ace kvs") + } + for i := range records[0] { + kv := strings.Split(records[0][i], "=") + switch kv[0] { + case "t": + e.Type = kv[1] + case "f": + e.Flags = kv[1] + case "p": + e.Permissions = kv[1] + case "s": + v, err := strconv.Atoi(kv[1]) + if err != nil { + return nil, err + } + e.ShareTime = v + case "c": + e.Creator = kv[1] + case "e": + v, err := strconv.Atoi(kv[1]) + if err != nil { + return nil, err + } + e.Expires = v + case "w": + e.Password = kv[1] + case "l": + e.Label = kv[1] + // TODO default ... log unknown keys? or add as opaque? hm we need that for tagged shares ... + } + } + return e, nil +} + +// Parse parses an acl string with the given delimiter (LongTextForm or ShortTextForm) +func getACEs(ctx context.Context, fsfn string, attrs []string) (entries []*ace, err error) { + log := appctx.GetLogger(ctx) + entries = []*ace{} + for i := range attrs { + if strings.HasPrefix(attrs[i], sharePrefix) { + principal := attrs[i][len(sharePrefix):] + var value []byte + if value, err = xattr.Get(fsfn, attrs[i]); err != nil { + log.Error().Err(err).Str("attr", attrs[i]).Msg("could not read attribute") + continue + } + var e *ace + if e, err = unmarshalACE(value); err != nil { + log.Error().Err(err).Str("attr", attrs[i]).Msg("could unmarshal ace") + continue + } + e.Principal = principal[2:] + // check consistency of Flags and principal type + if strings.Contains(e.Flags, "g") { + if principal[:1] != "g" { + log.Error().Str("attr", attrs[i]).Interface("ace", e).Msg("inconsistent ace: expected group") + continue + } + } else { + if principal[:1] != "u" { + log.Error().Str("attr", attrs[i]).Interface("ace", e).Msg("inconsistent ace: expected user") + continue + } + } + entries = append(entries, e) + } + } + return entries, nil +} + +func (fs *ocFS) ListGrants(ctx context.Context, ref *storageproviderv0alphapb.Reference) (grants []*storageproviderv0alphapb.Grant, err error) { + log := appctx.GetLogger(ctx) + var np string + if np, err = fs.resolve(ctx, ref); err != nil { + return nil, errors.Wrap(err, "ocFS: error resolving reference") + } + var attrs []string + if attrs, err = xattr.List(np); err != nil { + log.Error().Err(err).Msg("error listing attributes") + return nil, err + } + + log.Debug().Interface("attrs", attrs).Msg("read attributes") + // filter attributes + var aces []*ace + if aces, err = getACEs(ctx, np, attrs); err != nil { + log.Error().Err(err).Msg("error getting aces") + return nil, err + } + + grants = make([]*storageproviderv0alphapb.Grant, 0, len(aces)) + for i := range aces { + grantee := &storageproviderv0alphapb.Grantee{ + Id: &typespb.UserId{OpaqueId: aces[i].Principal}, + Type: fs.getGranteeType(aces[i]), + } + grants = append(grants, &storageproviderv0alphapb.Grant{ + Grantee: grantee, + Permissions: fs.getGrantPermissionSet(aces[i].Permissions), + }) + } + + return grants, nil +} + +func (fs *ocFS) getGranteeType(e *ace) storageproviderv0alphapb.GranteeType { + if strings.Contains(e.Flags, "g") { + return storageproviderv0alphapb.GranteeType_GRANTEE_TYPE_GROUP + } + return storageproviderv0alphapb.GranteeType_GRANTEE_TYPE_USER +} + +func (fs *ocFS) getGrantPermissionSet(mode string) *storageproviderv0alphapb.ResourcePermissions { + p := &storageproviderv0alphapb.ResourcePermissions{} + // r + if strings.Contains(mode, "r") { + p.Stat = true + p.InitiateFileDownload = true + p.ListContainer = true + } + // w + if strings.Contains(mode, "w") { + p.InitiateFileUpload = true + if p.InitiateFileDownload { + p.Move = true + } + } + //a + if strings.Contains(mode, "a") { + // TODO append data to file permission? + p.CreateContainer = true + } + //x + //if strings.Contains(mode, "x") { + // TODO execute file permission? + // TODO change directory permission? + //} + //d + if strings.Contains(mode, "d") { + p.Delete = true + } + //D ? + + // sharing + if strings.Contains(mode, "C") { + p.AddGrant = true + p.RemoveGrant = true + p.UpdateGrant = true + } + if strings.Contains(mode, "c") { + p.ListGrants = true + } + + // trash + if strings.Contains(mode, "u") { // u = undelete + p.ListRecycle = true + } + if strings.Contains(mode, "U") { + p.RestoreRecycleItem = true + } + if strings.Contains(mode, "P") { + p.PurgeRecycle = true + } + + // versions + if strings.Contains(mode, "v") { + p.ListFileVersions = true + } + if strings.Contains(mode, "V") { + p.RestoreFileVersion = true + } + + // ? + // TODO GetPath + if strings.Contains(mode, "q") { + p.GetQuota = true + } + // TODO set quota permission? + return p +} + +func (fs *ocFS) RemoveGrant(ctx context.Context, ref *storageproviderv0alphapb.Reference, g *storageproviderv0alphapb.Grant) (err error) { + + var np string + if np, err = fs.resolve(ctx, ref); err != nil { + return errors.Wrap(err, "ocFS: error resolving reference") + } + + var attr string + if g.Grantee.Type == storageproviderv0alphapb.GranteeType_GRANTEE_TYPE_GROUP { + attr = sharePrefix + "g:" + g.Grantee.Id.OpaqueId + } else { + attr = sharePrefix + "u:" + g.Grantee.Id.OpaqueId + } + + return xattr.Remove(np, attr) +} + +func (fs *ocFS) UpdateGrant(ctx context.Context, ref *storageproviderv0alphapb.Reference, g *storageproviderv0alphapb.Grant) error { + return fs.AddGrant(ctx, ref, g) +} + +func (fs *ocFS) GetQuota(ctx context.Context) (int, int, error) { + return 0, 0, nil +} + +func (fs *ocFS) CreateDir(ctx context.Context, fn string) (err error) { + np := fs.getInternalPath(ctx, fn) + if err = os.Mkdir(np, 0700); err != nil { + if os.IsNotExist(err) { + return errtypes.NotFound(fn) + } + // FIXME we also need already exists error, webdav expects 405 MethodNotAllowed + return errors.Wrap(err, "ocFS: error creating dir "+np) + } + return nil +} + +func (fs *ocFS) Delete(ctx context.Context, ref *storageproviderv0alphapb.Reference) (err error) { + var np string + if np, err = fs.resolve(ctx, ref); err != nil { + return errors.Wrap(err, "ocFS: error resolving reference") + } + if err = os.Remove(np); err != nil { + if os.IsNotExist(err) { + return errtypes.NotFound(fs.removeNamespace(ctx, np)) + } + // try recursive delete + if err = os.RemoveAll(np); err != nil { + return errors.Wrap(err, "ocFS: error deleting "+np) + } + } + return nil +} + +func (fs *ocFS) Move(ctx context.Context, oldRef, newRef *storageproviderv0alphapb.Reference) (err error) { + var oldName string + if oldName, err = fs.resolve(ctx, oldRef); err != nil { + return errors.Wrap(err, "ocFS: error resolving reference") + } + var newName string + if newName, err = fs.resolve(ctx, newRef); err != nil { + return errors.Wrap(err, "ocFS: error resolving reference") + } + if err = os.Rename(oldName, newName); err != nil { + return errors.Wrap(err, "ocFS: error moving "+oldName+" to "+newName) + } + return nil +} + +func (fs *ocFS) GetMD(ctx context.Context, ref *storageproviderv0alphapb.Reference) (*storageproviderv0alphapb.ResourceInfo, error) { + np, err := fs.resolve(ctx, ref) + if err != nil { + return nil, errors.Wrap(err, "ocFS: error resolving reference") + } + + fs.autocreate(ctx, np) + + md, err := os.Stat(np) + if err != nil { + if os.IsNotExist(err) { + return nil, errtypes.NotFound(fs.removeNamespace(ctx, np)) + } + return nil, errors.Wrap(err, "ocFS: error stating "+np) + } + c := fs.pool.Get() + defer c.Close() + m := fs.convertToResourceInfo(ctx, md, np, c) + + return m, nil +} + +func (fs *ocFS) ListFolder(ctx context.Context, ref *storageproviderv0alphapb.Reference) ([]*storageproviderv0alphapb.ResourceInfo, error) { + np, err := fs.resolve(ctx, ref) + if err != nil { + return nil, errors.Wrap(err, "ocFS: error resolving reference") + } + + fs.autocreate(ctx, np) + + mds, err := ioutil.ReadDir(np) + if err != nil { + if os.IsNotExist(err) { + return nil, errtypes.NotFound(fs.removeNamespace(ctx, np)) + } + return nil, errors.Wrap(err, "ocFS: error listing "+np) + } + + finfos := make([]*storageproviderv0alphapb.ResourceInfo, 0, len(mds)) + // TODO we should only open a connection if we need to set / store the fileid. no need to always open a connection when listing files + c := fs.pool.Get() + defer c.Close() + for i := range mds { + p := path.Join(np, mds[i].Name()) + m := fs.convertToResourceInfo(ctx, mds[i], p, c) + finfos = append(finfos, m) + } + return finfos, nil +} + +func (fs *ocFS) Upload(ctx context.Context, ref *storageproviderv0alphapb.Reference, r io.ReadCloser) error { + np, err := fs.resolve(ctx, ref) + if err != nil { + return errors.Wrap(err, "ocFS: error resolving reference") + } + + // we cannot rely on /tmp as it can live in another partition and we can + // hit invalid cross-device link errors, so we create the tmp file in the same directory + // the file is supposed to be written. + tmp, err := ioutil.TempFile(path.Dir(np), "._reva_atomic_upload") + if err != nil { + return errors.Wrap(err, "ocFS: error creating tmp fn at "+path.Dir(np)) + } + + _, err = io.Copy(tmp, r) + if err != nil { + return errors.Wrap(err, "ocFS: error writing to tmp file "+tmp.Name()) + } + + // TODO(jfd): copy attributes of existing file to tmp file? + if err := fs.copyMD(np, tmp.Name()); err != nil { + return errors.Wrap(err, "ocFS: error copying metadata from "+np+" to "+tmp.Name()) + } + + // create revision if destination exists + if _, err := os.Stat(np); err != nil { + if err := fs.archiveRevision(ctx, np); err != nil { + return err + } + } + + // TODO(jfd): make sure rename is atomic, missing fsync ... + if err := os.Rename(tmp.Name(), np); err != nil { + return errors.Wrap(err, "ocFS: error renaming from "+tmp.Name()+" to "+np) + } + + return nil +} + +func (fs *ocFS) archiveRevision(ctx context.Context, np string) error { + // move existing file to versions dir + vp := fmt.Sprintf("%s.v%d", fs.getVersionsPath(ctx, np), time.Now().Unix()) + if err := os.MkdirAll(path.Dir(vp), 0700); err != nil { + return errors.Wrap(err, "ocFS: error creating versions dir "+vp) + } + + // TODO(jfd): make sure rename is atomic, missing fsync ... + if err := os.Rename(np, vp); err != nil { + return errors.Wrap(err, "ocFS: error renaming from "+np+" to "+vp) + } + + return nil +} + +func (fs *ocFS) copyMD(s string, t string) (err error) { + var attrs []string + if attrs, err = xattr.List(s); err != nil { + return err + } + for i := range attrs { + if strings.HasPrefix(attrs[i], "user.oc.") { + var d []byte + if d, err = xattr.Get(s, attrs[i]); err != nil { + return err + } + if err = xattr.Set(t, attrs[i], d); err != nil { + return err + } + } + } + return nil +} + +func (fs *ocFS) Download(ctx context.Context, ref *storageproviderv0alphapb.Reference) (io.ReadCloser, error) { + np, err := fs.resolve(ctx, ref) + if err != nil { + return nil, errors.Wrap(err, "ocFS: error resolving reference") + } + r, err := os.Open(np) + if err != nil { + if os.IsNotExist(err) { + return nil, errtypes.NotFound(fs.removeNamespace(ctx, np)) + } + return nil, errors.Wrap(err, "ocFS: error reading "+np) + } + return r, nil +} + +func (fs *ocFS) ListRevisions(ctx context.Context, ref *storageproviderv0alphapb.Reference) ([]*storageproviderv0alphapb.FileVersion, error) { + np, err := fs.resolve(ctx, ref) + if err != nil { + return nil, errors.Wrap(err, "ocFS: error resolving reference") + } + vp := fs.getVersionsPath(ctx, np) + + fs.autocreate(ctx, vp) + + bn := path.Base(np) + + revisions := []*storageproviderv0alphapb.FileVersion{} + mds, err := ioutil.ReadDir(path.Dir(vp)) + if err != nil { + return nil, errors.Wrap(err, "ocFS: error reading"+path.Dir(vp)) + } + for i := range mds { + rev := fs.filterAsRevision(ctx, bn, mds[i]) + if rev != nil { + revisions = append(revisions, rev) + } + + } + return revisions, nil +} + +func (fs *ocFS) filterAsRevision(ctx context.Context, bn string, md os.FileInfo) *storageproviderv0alphapb.FileVersion { + if strings.HasPrefix(md.Name(), bn) { + // versions have filename.ext.v12345678 + version := md.Name()[len(bn)+2:] // truncate ".v" to get version mtime + mtime, err := strconv.Atoi(version) + if err != nil { + log := appctx.GetLogger(ctx) + log.Error().Err(err).Str("path", md.Name()).Msg("invalid version mtime") + return nil + } + // TODO(jfd) trashed versions are in the files_trashbin/versions folder ... not relevant here + return &storageproviderv0alphapb.FileVersion{ + Key: version, + Size: uint64(md.Size()), + Mtime: uint64(mtime), + } + } + return nil +} + +func (fs *ocFS) DownloadRevision(ctx context.Context, ref *storageproviderv0alphapb.Reference, revisionKey string) (io.ReadCloser, error) { + return nil, errtypes.NotSupported("download revision") +} + +func (fs *ocFS) RestoreRevision(ctx context.Context, ref *storageproviderv0alphapb.Reference, revisionKey string) error { + np, err := fs.resolve(ctx, ref) + if err != nil { + return errors.Wrap(err, "ocFS: error resolving reference") + } + vp := fs.getVersionsPath(ctx, np) + rp := vp + ".v" + revisionKey + + // check revision exists + rs, err := os.Stat(rp) + if err != nil { + return err + } + + if !rs.Mode().IsRegular() { + return fmt.Errorf("%s is not a regular file", rp) + } + + source, err := os.Open(rp) + if err != nil { + return err + } + defer source.Close() + + // destination should be available, otherwise we could not have navigated to its revisions + if err := fs.archiveRevision(ctx, np); err != nil { + return err + } + + destination, err := os.Create(np) + if err != nil { + // TODO(jfd) bring back revision in case sth goes wrong? + return err + } + defer destination.Close() + + _, err = io.Copy(destination, source) + // TODO(jfd) bring back revision in case sth goes wrong? + return err +} + +func (fs *ocFS) EmptyRecycle(ctx context.Context) error { + return errtypes.NotSupported("empty recycle") +} + +func (fs *ocFS) ListRecycle(ctx context.Context) ([]*storageproviderv0alphapb.RecycleItem, error) { + return nil, errtypes.NotSupported("list recycle") +} + +func (fs *ocFS) RestoreRecycleItem(ctx context.Context, key string) error { + return errtypes.NotSupported("restore recycle") +} diff --git a/pkg/user/manager/json/json.go b/pkg/user/manager/json/json.go index 274ebd9a3a..562fd7862b 100644 --- a/pkg/user/manager/json/json.go +++ b/pkg/user/manager/json/json.go @@ -82,14 +82,15 @@ func New(m map[string]interface{}) (user.Manager, error) { func (m *manager) GetUser(ctx context.Context, uid *typespb.UserId) (*authv0alphapb.User, error) { for _, u := range m.users { - if u.Username == uid.OpaqueId { + // TODO(jfd) we should also compare idp / iss? + if u.Id.GetOpaqueId() == uid.OpaqueId { return u, nil } } return nil, errtypes.NotFound(uid.OpaqueId) } -// TODO search Opaque? compare sub? +// TODO(jfd) search Opaque? compare sub? func userContains(u *authv0alphapb.User, query string) bool { return strings.Contains(u.Username, query) || strings.Contains(u.DisplayName, query) || strings.Contains(u.Mail, query) } diff --git a/pkg/user/manager/oidc/oidc.go b/pkg/user/manager/oidc/oidc.go index a8c309d76a..c4fb19fca6 100644 --- a/pkg/user/manager/oidc/oidc.go +++ b/pkg/user/manager/oidc/oidc.go @@ -50,8 +50,13 @@ func (m *manager) GetUser(ctx context.Context, uid *typespb.UserId) (*authv0alph } user := &authv0alphapb.User{ - Subject: claims.Sub, // a stable non reassignable id - Issuer: claims.Iss, // in the scope of this issuer + // TODO(jfd) clean up idp = iss, sub = opaque ... is redundant + Id: &typespb.UserId{ + OpaqueId: claims.Sub, // a stable non reassignable id + Idp: claims.Iss, // in the scope of this issuer + }, + // Subject: claims.Sub, // TODO(labkode) remove from CS3, is in Id + // Issuer: claims.Iss, // TODO(labkode) remove from CS3, is in Id Username: claims.PreferredUsername, Groups: []string{}, Mail: claims.Email,