diff --git a/changelog/unreleased/app-passwords.md b/changelog/unreleased/app-passwords.md new file mode 100644 index 0000000000..6ecebdd1d4 --- /dev/null +++ b/changelog/unreleased/app-passwords.md @@ -0,0 +1,9 @@ +Enhancement: Application passwords management + +This PR adds the functionality to generate authentication tokens with limited +scope on behalf of registered users. These can be used in third party apps or in +case primary user credentials cannot be submitted to other parties. + +https://github.com/cs3org/reva/pull/1719 +https://github.com/cs3org/reva/issues/1714 +https://github.com/cs3org/cs3apis/pull/127 diff --git a/cmd/revad/runtime/loader.go b/cmd/revad/runtime/loader.go index 35e8406204..0b90f93a6a 100644 --- a/cmd/revad/runtime/loader.go +++ b/cmd/revad/runtime/loader.go @@ -27,6 +27,7 @@ import ( _ "github.com/cs3org/reva/internal/http/interceptors/auth/tokenwriter/loader" _ "github.com/cs3org/reva/internal/http/interceptors/loader" _ "github.com/cs3org/reva/internal/http/services/loader" + _ "github.com/cs3org/reva/pkg/appauth/manager/loader" _ "github.com/cs3org/reva/pkg/auth/manager/loader" _ "github.com/cs3org/reva/pkg/auth/registry/loader" _ "github.com/cs3org/reva/pkg/cbox/loader" diff --git a/go.mod b/go.mod index 079168da36..e2de560f0c 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,7 @@ module github.com/cs3org/reva require ( + bou.ke/monkey v1.0.2 contrib.go.opencensus.io/exporter/jaeger v0.2.1 contrib.go.opencensus.io/exporter/prometheus v0.3.0 github.com/BurntSushi/toml v0.3.1 @@ -14,9 +15,10 @@ require ( github.com/cheggaaa/pb v1.0.29 github.com/coreos/go-oidc v2.2.1+incompatible github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e - github.com/cs3org/go-cs3apis v0.0.0-20210507060801-f176760d55f4 + github.com/cs3org/go-cs3apis v0.0.0-20210527092012-82617367e09d github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/eventials/go-tus v0.0.0-20200718001131-45c7ec8f5d59 + github.com/gdexlab/go-render v1.0.1 github.com/go-ldap/ldap/v3 v3.3.0 github.com/go-sql-driver/mysql v1.6.0 github.com/golang/protobuf v1.5.2 @@ -41,6 +43,7 @@ require ( github.com/rs/cors v1.7.0 github.com/rs/zerolog v1.22.0 github.com/sciencemesh/meshdirectory-web v1.0.4 + github.com/sethvargo/go-password v0.2.0 github.com/stretchr/testify v1.7.0 github.com/studio-b12/gowebdav v0.0.0-20200303150724-9380631c29a1 github.com/tus/tusd v1.1.1-0.20200416115059-9deabf9d80c2 diff --git a/go.sum b/go.sum index dbb0a6abbb..00ae3c64c4 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= +bou.ke/monkey v1.0.2 h1:kWcnsrCNUatbxncxR/ThdYqbytgOIArtYWqcQLQzKLI= +bou.ke/monkey v1.0.2/go.mod h1:OqickVX3tNx6t33n1xvtTtu85YN5s6cKwVug+oHMaIA= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.40.0/go.mod h1:Tk58MuI9rbLMKlAjeO/bDnteAx7tX2gJIXw4T5Jwlro= @@ -148,8 +150,8 @@ github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7Do github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e h1:tqSPWQeueWTKnJVMJffz4pz0o1WuQxJ28+5x5JgaHD8= github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e/go.mod h1:XJEZ3/EQuI3BXTp/6DUzFr850vlxq11I6satRtz0YQ4= -github.com/cs3org/go-cs3apis v0.0.0-20210507060801-f176760d55f4 h1:lihiUwqal+sO+57VTHGRvHbI9baN+D85fPZG2N1Sk6s= -github.com/cs3org/go-cs3apis v0.0.0-20210507060801-f176760d55f4/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY= +github.com/cs3org/go-cs3apis v0.0.0-20210527092012-82617367e09d h1:mQiARNvWPIPdqal7gWWTIVpC6C10aC9/2fVjCK9AIg4= +github.com/cs3org/go-cs3apis v0.0.0-20210527092012-82617367e09d/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY= github.com/cucumber/godog v0.8.1/go.mod h1:vSh3r/lM+psC1BPXvdkSEuNjmXfpVqrMGYAElF6hxnA= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -192,6 +194,8 @@ github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/gdexlab/go-render v1.0.1 h1:rxqB3vo5s4n1kF0ySmoNeSPRYkEsyHgln4jFIQY7v0U= +github.com/gdexlab/go-render v1.0.1/go.mod h1:wRi5nW2qfjiGj4mPukH4UV0IknS1cHD4VgFTmJX5JzM= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= @@ -1078,6 +1082,8 @@ github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516/go.mod h1:Yow6lPLS github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sethgrid/pester v0.0.0-20190127155807-68a33a018ad0/go.mod h1:Ad7IjTpvzZO8Fl0vh9AzQ+j/jYZfyp2diGwI8m5q+ns= +github.com/sethvargo/go-password v0.2.0 h1:BTDl4CC/gjf/axHMaDQtw507ogrXLci6XRiLc7i/UHI= +github.com/sethvargo/go-password v0.2.0/go.mod h1:Ym4Mr9JXLBycr02MFuVQ/0JHidNetSgbzutTr3zsYXE= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= diff --git a/internal/grpc/interceptors/auth/auth.go b/internal/grpc/interceptors/auth/auth.go index 9a2cda1bb7..a611023c56 100644 --- a/internal/grpc/interceptors/auth/auth.go +++ b/internal/grpc/interceptors/auth/auth.go @@ -242,7 +242,14 @@ func dismantleToken(ctx context.Context, tkn string, req interface{}, mgr token. // for OCM shares, guest accounts, etc. log.Info().Msgf("resolving path reference to ID to check token scope %+v", ref.GetPath()) var share link.PublicShare - err = utils.UnmarshalJSONToProtoV1(tokenScope["publicshare"].Resource.Value, &share) + var publicShareScope []byte + for k := range tokenScope { + if strings.HasPrefix(k, "publicshare") { + publicShareScope = tokenScope[k].Resource.Value + break + } + } + err = utils.UnmarshalJSONToProtoV1(publicShareScope, &share) if err != nil { return nil, err } diff --git a/internal/grpc/services/applicationauth/applicationauth.go b/internal/grpc/services/applicationauth/applicationauth.go new file mode 100644 index 0000000000..cccf72e8e2 --- /dev/null +++ b/internal/grpc/services/applicationauth/applicationauth.go @@ -0,0 +1,158 @@ +// Copyright 2018-2021 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package applicationauth + +import ( + "context" + + appauthpb "github.com/cs3org/go-cs3apis/cs3/auth/applications/v1beta1" + "github.com/cs3org/reva/pkg/appauth" + "github.com/cs3org/reva/pkg/appauth/manager/registry" + "github.com/cs3org/reva/pkg/errtypes" + "github.com/cs3org/reva/pkg/rgrpc" + "github.com/cs3org/reva/pkg/rgrpc/status" + "github.com/mitchellh/mapstructure" + "github.com/pkg/errors" + "google.golang.org/grpc" +) + +func init() { + rgrpc.Register("applicationauth", New) +} + +type config struct { + Driver string `mapstructure:"driver"` + Drivers map[string]map[string]interface{} `mapstructure:"drivers"` +} + +type service struct { + conf *config + am appauth.Manager +} + +func (c *config) init() { + if c.Driver == "" { + c.Driver = "json" + } +} + +func (s *service) Register(ss *grpc.Server) { + appauthpb.RegisterApplicationsAPIServer(ss, s) +} + +func getAppAuthManager(c *config) (appauth.Manager, error) { + if f, ok := registry.NewFuncs[c.Driver]; ok { + return f(c.Drivers[c.Driver]) + } + return nil, errtypes.NotFound("driver not found: " + c.Driver) +} + +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 +} + +// New creates a app auth provider svc +func New(m map[string]interface{}, ss *grpc.Server) (rgrpc.Service, error) { + + c, err := parseConfig(m) + if err != nil { + return nil, err + } + c.init() + + am, err := getAppAuthManager(c) + if err != nil { + return nil, err + } + + service := &service{ + conf: c, + am: am, + } + + return service, nil +} + +func (s *service) Close() error { + return nil +} + +func (s *service) UnprotectedEndpoints() []string { + return []string{"/cs3.auth.applications.v1beta1.ApplicationsAPI/GetAppPassword"} +} + +func (s *service) GenerateAppPassword(ctx context.Context, req *appauthpb.GenerateAppPasswordRequest) (*appauthpb.GenerateAppPasswordResponse, error) { + pwd, err := s.am.GenerateAppPassword(ctx, req.TokenScope, req.Label, req.Expiration) + if err != nil { + return &appauthpb.GenerateAppPasswordResponse{ + Status: status.NewInternal(ctx, err, "error generating app password"), + }, nil + } + + return &appauthpb.GenerateAppPasswordResponse{ + Status: status.NewOK(ctx), + AppPassword: pwd, + }, nil +} + +func (s *service) ListAppPasswords(ctx context.Context, req *appauthpb.ListAppPasswordsRequest) (*appauthpb.ListAppPasswordsResponse, error) { + pwds, err := s.am.ListAppPasswords(ctx) + if err != nil { + return &appauthpb.ListAppPasswordsResponse{ + Status: status.NewInternal(ctx, err, "error listing app passwords"), + }, nil + } + + return &appauthpb.ListAppPasswordsResponse{ + Status: status.NewOK(ctx), + AppPasswords: pwds, + }, nil +} + +func (s *service) InvalidateAppPassword(ctx context.Context, req *appauthpb.InvalidateAppPasswordRequest) (*appauthpb.InvalidateAppPasswordResponse, error) { + err := s.am.InvalidateAppPassword(ctx, req.Password) + if err != nil { + return &appauthpb.InvalidateAppPasswordResponse{ + Status: status.NewInternal(ctx, err, "error invalidating app password"), + }, nil + } + + return &appauthpb.InvalidateAppPasswordResponse{ + Status: status.NewOK(ctx), + }, nil +} + +func (s *service) GetAppPassword(ctx context.Context, req *appauthpb.GetAppPasswordRequest) (*appauthpb.GetAppPasswordResponse, error) { + pwd, err := s.am.GetAppPassword(ctx, req.User, req.Password) + if err != nil { + return &appauthpb.GetAppPasswordResponse{ + Status: status.NewInternal(ctx, err, "error getting app password via username/password"), + }, nil + } + + return &appauthpb.GetAppPasswordResponse{ + Status: status.NewOK(ctx), + AppPassword: pwd, + }, nil +} diff --git a/internal/grpc/services/gateway/applicationauth.go b/internal/grpc/services/gateway/applicationauth.go new file mode 100644 index 0000000000..869b68fc04 --- /dev/null +++ b/internal/grpc/services/gateway/applicationauth.go @@ -0,0 +1,96 @@ +// Copyright 2018-2021 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package gateway + +import ( + "context" + + appauthpb "github.com/cs3org/go-cs3apis/cs3/auth/applications/v1beta1" + "github.com/cs3org/reva/pkg/rgrpc/status" + "github.com/cs3org/reva/pkg/rgrpc/todo/pool" + "github.com/pkg/errors" +) + +func (s *svc) GenerateAppPassword(ctx context.Context, req *appauthpb.GenerateAppPasswordRequest) (*appauthpb.GenerateAppPasswordResponse, error) { + c, err := pool.GetAppAuthProviderServiceClient(s.c.ApplicationAuthEndpoint) + if err != nil { + err = errors.Wrap(err, "gateway: error calling GetAppAuthProviderServiceClient") + return &appauthpb.GenerateAppPasswordResponse{ + Status: status.NewInternal(ctx, err, "error getting app auth provider client"), + }, nil + } + + res, err := c.GenerateAppPassword(ctx, req) + if err != nil { + return nil, errors.Wrap(err, "gateway: error calling GenerateAppPassword") + } + + return res, nil +} + +func (s *svc) ListAppPasswords(ctx context.Context, req *appauthpb.ListAppPasswordsRequest) (*appauthpb.ListAppPasswordsResponse, error) { + c, err := pool.GetAppAuthProviderServiceClient(s.c.ApplicationAuthEndpoint) + if err != nil { + err = errors.Wrap(err, "gateway: error calling GetAppAuthProviderServiceClient") + return &appauthpb.ListAppPasswordsResponse{ + Status: status.NewInternal(ctx, err, "error getting app auth provider client"), + }, nil + } + + res, err := c.ListAppPasswords(ctx, req) + if err != nil { + return nil, errors.Wrap(err, "gateway: error calling ListAppPasswords") + } + + return res, nil +} + +func (s *svc) InvalidateAppPassword(ctx context.Context, req *appauthpb.InvalidateAppPasswordRequest) (*appauthpb.InvalidateAppPasswordResponse, error) { + c, err := pool.GetAppAuthProviderServiceClient(s.c.ApplicationAuthEndpoint) + if err != nil { + err = errors.Wrap(err, "gateway: error calling GetAppAuthProviderServiceClient") + return &appauthpb.InvalidateAppPasswordResponse{ + Status: status.NewInternal(ctx, err, "error getting app auth provider client"), + }, nil + } + + res, err := c.InvalidateAppPassword(ctx, req) + if err != nil { + return nil, errors.Wrap(err, "gateway: error calling InvalidateAppPassword") + } + + return res, nil +} + +func (s *svc) GetAppPassword(ctx context.Context, req *appauthpb.GetAppPasswordRequest) (*appauthpb.GetAppPasswordResponse, error) { + c, err := pool.GetAppAuthProviderServiceClient(s.c.ApplicationAuthEndpoint) + if err != nil { + err = errors.Wrap(err, "gateway: error calling GetAppAuthProviderServiceClient") + return &appauthpb.GetAppPasswordResponse{ + Status: status.NewInternal(ctx, err, "error getting app auth provider client"), + }, nil + } + + res, err := c.GetAppPassword(ctx, req) + if err != nil { + return nil, errors.Wrap(err, "gateway: error calling GetAppPassword") + } + + return res, nil +} diff --git a/internal/grpc/services/gateway/gateway.go b/internal/grpc/services/gateway/gateway.go index 80420e04a0..981fee2fa3 100644 --- a/internal/grpc/services/gateway/gateway.go +++ b/internal/grpc/services/gateway/gateway.go @@ -43,6 +43,7 @@ func init() { type config struct { AuthRegistryEndpoint string `mapstructure:"authregistrysvc"` + ApplicationAuthEndpoint string `mapstructure:"applicationauthsvc"` StorageRegistryEndpoint string `mapstructure:"storageregistrysvc"` AppRegistryEndpoint string `mapstructure:"appregistrysvc"` PreferencesEndpoint string `mapstructure:"preferencessvc"` @@ -89,6 +90,7 @@ func (c *config) init() { // if services address are not specified we used the shared conf // for the gatewaysvc to have dev setups very quickly. c.AuthRegistryEndpoint = sharedconf.GetGatewaySVC(c.AuthRegistryEndpoint) + c.ApplicationAuthEndpoint = sharedconf.GetGatewaySVC(c.ApplicationAuthEndpoint) c.StorageRegistryEndpoint = sharedconf.GetGatewaySVC(c.StorageRegistryEndpoint) c.AppRegistryEndpoint = sharedconf.GetGatewaySVC(c.AppRegistryEndpoint) c.PreferencesEndpoint = sharedconf.GetGatewaySVC(c.PreferencesEndpoint) diff --git a/internal/grpc/services/loader/loader.go b/internal/grpc/services/loader/loader.go index 253765573b..118eeed39e 100644 --- a/internal/grpc/services/loader/loader.go +++ b/internal/grpc/services/loader/loader.go @@ -20,6 +20,7 @@ package loader import ( // Load core gRPC services. + _ "github.com/cs3org/reva/internal/grpc/services/applicationauth" _ "github.com/cs3org/reva/internal/grpc/services/appprovider" _ "github.com/cs3org/reva/internal/grpc/services/appregistry" _ "github.com/cs3org/reva/internal/grpc/services/authprovider" diff --git a/internal/grpc/services/userprovider/userprovider.go b/internal/grpc/services/userprovider/userprovider.go index fb244990de..465185d4e6 100644 --- a/internal/grpc/services/userprovider/userprovider.go +++ b/internal/grpc/services/userprovider/userprovider.go @@ -92,7 +92,7 @@ func (s *service) Close() error { } func (s *service) UnprotectedEndpoints() []string { - return []string{"/cs3.identity.user.v1beta1.UserAPI/GetUser"} + return []string{"/cs3.identity.user.v1beta1.UserAPI/GetUser", "/cs3.identity.user.v1beta1.UserAPI/GetUserByClaim"} } func (s *service) Register(ss *grpc.Server) { diff --git a/pkg/appauth/appauth.go b/pkg/appauth/appauth.go new file mode 100644 index 0000000000..eb47857a8c --- /dev/null +++ b/pkg/appauth/appauth.go @@ -0,0 +1,41 @@ +// Copyright 2018-2021 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package appauth + +import ( + "context" + + apppb "github.com/cs3org/go-cs3apis/cs3/auth/applications/v1beta1" + authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1" + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" +) + +// Manager is the interface that manages application authentication mechanisms. +type Manager interface { + // GenerateAppPassword creates a password with specified scope to be used by + // third-party applications. + GenerateAppPassword(ctx context.Context, scope map[string]*authpb.Scope, label string, expiration *typespb.Timestamp) (*apppb.AppPassword, error) + // ListAppPasswords lists the application passwords created by a user. + ListAppPasswords(ctx context.Context) ([]*apppb.AppPassword, error) + // InvalidateAppPassword invalidates a generated password. + InvalidateAppPassword(ctx context.Context, secret string) error + // GetAppPassword retrieves the password information by the combination of username and password. + GetAppPassword(ctx context.Context, user *userpb.UserId, secret string) (*apppb.AppPassword, error) +} diff --git a/pkg/appauth/manager/json/json.go b/pkg/appauth/manager/json/json.go new file mode 100644 index 0000000000..1449adcac9 --- /dev/null +++ b/pkg/appauth/manager/json/json.go @@ -0,0 +1,233 @@ +// Copyright 2018-2021 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package json + +import ( + "context" + "encoding/json" + "io/ioutil" + "os" + "sync" + "time" + + apppb "github.com/cs3org/go-cs3apis/cs3/auth/applications/v1beta1" + authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1" + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + "github.com/cs3org/reva/pkg/appauth" + "github.com/cs3org/reva/pkg/appauth/manager/registry" + "github.com/cs3org/reva/pkg/errtypes" + "github.com/cs3org/reva/pkg/user" + "github.com/mitchellh/mapstructure" + "github.com/pkg/errors" + "github.com/sethvargo/go-password/password" +) + +func init() { + registry.Register("json", New) +} + +type config struct { + File string `mapstructure:"file"` + TokenStrength int `mapstructure:"token_strength"` +} + +type jsonManager struct { + sync.Mutex + config *config + // map[userid][password]AppPassword + passwords map[string]map[string]*apppb.AppPassword +} + +// New returns a new mgr. +func New(m map[string]interface{}) (appauth.Manager, error) { + c, err := parseConfig(m) + if err != nil { + return nil, errors.Wrap(err, "error creating a new manager") + } + + c.init() + + // load or create file + manager, err := loadOrCreate(c.File) + if err != nil { + return nil, errors.Wrap(err, "error loading the file containing the application passwords") + } + + manager.config = c + + return manager, nil +} + +func (c *config) init() { + if c.File == "" { + c.File = "/var/tmp/reva/appauth.json" + } +} + +func parseConfig(m map[string]interface{}) (*config, error) { + c := &config{} + if err := mapstructure.Decode(m, c); err != nil { + return nil, err + } + return c, nil +} + +func loadOrCreate(file string) (*jsonManager, error) { + stat, err := os.Stat(file) + if os.IsNotExist(err) || stat.Size() == 0 { + if err = ioutil.WriteFile(file, []byte("{}"), 0644); err != nil { + return nil, errors.Wrapf(err, "error creating the file %s", file) + } + } + + fd, err := os.OpenFile(file, os.O_RDONLY, 0) + if err != nil { + return nil, errors.Wrapf(err, "error opening the file %s", file) + } + defer fd.Close() + + data, err := ioutil.ReadAll(fd) + if err != nil { + return nil, errors.Wrapf(err, "error reading the file %s", file) + } + + m := &jsonManager{} + if err = json.Unmarshal(data, &m.passwords); err != nil { + return nil, errors.Wrapf(err, "error parsing the file %s", file) + } + + if m.passwords == nil { + m.passwords = make(map[string]map[string]*apppb.AppPassword) + } + + return m, nil +} + +func (mgr *jsonManager) GenerateAppPassword(ctx context.Context, scope map[string]*authpb.Scope, label string, expiration *typespb.Timestamp) (*apppb.AppPassword, error) { + token, err := password.Generate(mgr.config.TokenStrength, 10, 10, false, false) + if err != nil { + return nil, errors.Wrap(err, "error creating new token") + } + userID := user.ContextMustGetUser(ctx).GetId() + ctime := now() + + appPass := &apppb.AppPassword{ + Password: token, + TokenScope: scope, + Label: label, + Expiration: expiration, + Ctime: ctime, + Utime: ctime, + User: userID, + } + mgr.Lock() + defer mgr.Unlock() + + // check if user has some previous password + if _, ok := mgr.passwords[userID.String()]; !ok { + mgr.passwords[userID.String()] = make(map[string]*apppb.AppPassword) + } + + mgr.passwords[userID.String()][token] = appPass + + err = mgr.save() + if err != nil { + return nil, errors.Wrap(err, "error saving new token") + } + + return appPass, nil +} + +func (mgr *jsonManager) ListAppPasswords(ctx context.Context) ([]*apppb.AppPassword, error) { + userID := user.ContextMustGetUser(ctx).GetId() + mgr.Lock() + defer mgr.Unlock() + appPasswords := []*apppb.AppPassword{} + for _, pw := range mgr.passwords[userID.String()] { + appPasswords = append(appPasswords, pw) + } + return appPasswords, nil +} + +func (mgr *jsonManager) InvalidateAppPassword(ctx context.Context, password string) error { + userID := user.ContextMustGetUser(ctx).GetId() + mgr.Lock() + defer mgr.Unlock() + + // see if user has a list of passwords + appPasswords, ok := mgr.passwords[userID.String()] + if !ok || len(appPasswords) == 0 { + return errtypes.NotFound("password not found") + } + + if _, ok := appPasswords[password]; !ok { + return errtypes.NotFound("password not found") + } + delete(mgr.passwords[userID.String()], password) + + // if user has 0 passwords, delete user key from state map + if len(mgr.passwords[userID.String()]) == 0 { + delete(mgr.passwords, userID.String()) + } + + return mgr.save() +} + +func (mgr *jsonManager) GetAppPassword(ctx context.Context, userID *userpb.UserId, password string) (*apppb.AppPassword, error) { + mgr.Lock() + defer mgr.Unlock() + + appPassword, ok := mgr.passwords[userID.String()] + if !ok { + return nil, errtypes.NotFound("password not found") + } + + pw, ok := appPassword[password] + if !ok { + return nil, errtypes.NotFound("password not found") + } + + if pw.Expiration != nil && pw.Expiration.Seconds != 0 && uint64(time.Now().Unix()) > pw.Expiration.Seconds { + return nil, errtypes.NotFound("password not found") + } + + pw.Utime = now() + if err := mgr.save(); err != nil { + return nil, errors.Wrap(err, "error saving file") + } + return pw, nil +} + +func now() *typespb.Timestamp { + return &typespb.Timestamp{Seconds: uint64(time.Now().Unix())} +} + +func (mgr *jsonManager) save() error { + data, err := json.Marshal(mgr.passwords) + if err != nil { + return errors.Wrap(err, "error encoding json file") + } + + if err = ioutil.WriteFile(mgr.config.File, data, 0644); err != nil { + return errors.Wrapf(err, "error writing to file %s", mgr.config.File) + } + + return nil +} diff --git a/pkg/appauth/manager/json/json_test.go b/pkg/appauth/manager/json/json_test.go new file mode 100644 index 0000000000..e42d73f5b5 --- /dev/null +++ b/pkg/appauth/manager/json/json_test.go @@ -0,0 +1,685 @@ +// Copyright 2018-2021 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package json + +import ( + "context" + "encoding/json" + "io/ioutil" + "os" + "reflect" + "testing" + "time" + + "bou.ke/monkey" + apppb "github.com/cs3org/go-cs3apis/cs3/auth/applications/v1beta1" + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + "github.com/cs3org/reva/pkg/user" + "github.com/gdexlab/go-render/render" + "github.com/sethvargo/go-password/password" +) + +func TestNewManager(t *testing.T) { + userTest := &userpb.User{Id: &userpb.UserId{Idp: "0"}, Username: "Test User"} + + // temp directory where are stored tests config files + tempDir := createTempDir(t, "jsonappauth_test") + defer os.RemoveAll(tempDir) + + jsonCorruptedFile := createTempFile(t, tempDir, "corrupted.json") + defer jsonCorruptedFile.Close() + jsonEmptyFile := createTempFile(t, tempDir, "empty.json") + defer jsonEmptyFile.Close() + jsonOkFile := createTempFile(t, tempDir, "ok.json") + defer jsonOkFile.Close() + + dummyData := map[string]map[string]*apppb.AppPassword{ + userTest.GetId().String(): { + "1234": { + Password: "1234", + TokenScope: nil, + Label: "label", + User: userTest.GetId(), + Expiration: nil, + Ctime: &typespb.Timestamp{Seconds: 0}, + Utime: &typespb.Timestamp{Seconds: 0}, + }, + }} + + dummyDataJSON, _ := json.Marshal(dummyData) + + // fill temp file with tests data + fill(t, jsonCorruptedFile, `[{`) + fill(t, jsonEmptyFile, "") + fill(t, jsonOkFile, string(dummyDataJSON)) + + testCases := []struct { + description string + configMap map[string]interface{} + expected *jsonManager + }{ + { + description: "New appauth manager from corrupted state file", + configMap: map[string]interface{}{ + "file": jsonCorruptedFile.Name(), + "token_strength": 10, + }, + expected: nil, // nil == error + }, + { + description: "New appauth manager from empty state file", + configMap: map[string]interface{}{ + "file": jsonEmptyFile.Name(), + "token_strength": 10, + }, + expected: &jsonManager{ + config: &config{ + File: jsonEmptyFile.Name(), + TokenStrength: 10, + }, + passwords: map[string]map[string]*apppb.AppPassword{}, + }, + }, + { + description: "New appauth manager from state file", + configMap: map[string]interface{}{ + "file": jsonOkFile.Name(), + "token_strength": 10, + }, + expected: &jsonManager{ + config: &config{ + File: jsonOkFile.Name(), + TokenStrength: 10, + }, + passwords: dummyData, + }, + }, + } + + for _, test := range testCases { + t.Run(test.description, func(t *testing.T) { + manager, err := New(test.configMap) + if test.expected == nil { + if err == nil { + t.Fatalf("no error (but we expected one) while get manager") + } else { + t.Skip() + } + } + if !reflect.DeepEqual(test.expected, manager) { + t.Fatalf("appauth differ: expected=%v got=%v", render.AsCode(test.expected), render.AsCode(manager)) + } + }) + } + +} + +func TestGenerateAppPassword(t *testing.T) { + userTest := &userpb.User{Id: &userpb.UserId{Idp: "0"}, Username: "Test User"} + ctx := user.ContextSetUser(context.Background(), userTest) + tempDir := createTempDir(t, "jsonappauth_test") + defer os.RemoveAll(tempDir) + + nowFixed := time.Date(2021, time.May, 21, 12, 21, 0, 0, time.UTC) + patchNow := monkey.Patch(time.Now, func() time.Time { return nowFixed }) + now := now() + token := "1234" + patchPasswordGenerate := monkey.Patch(password.Generate, func(int, int, int, bool, bool) (string, error) { return token, nil }) + defer patchNow.Unpatch() + defer patchPasswordGenerate.Unpatch() + + dummyData := map[string]map[string]*apppb.AppPassword{ + userpb.User{Id: &userpb.UserId{Idp: "1"}, Username: "Test User1"}.Id.String(): { + "XXXX": { + Password: "XXXX", + Label: "", + User: &userpb.UserId{Idp: "1"}, + Ctime: now, + Utime: now, + }, + }, + } + + dummyDataJSON, _ := json.Marshal(dummyData) + + testCases := []struct { + description string + prevStateJSON string + expectedState map[string]map[string]*apppb.AppPassword + }{ + { + description: "GenerateAppPassword with empty state", + prevStateJSON: `{}`, + expectedState: map[string]map[string]*apppb.AppPassword{ + userTest.GetId().String(): { + token: { + Password: token, + TokenScope: nil, + Label: "label", + User: userTest.GetId(), + Expiration: nil, + Ctime: now, + Utime: now, + }, + }, + }, + }, + { + description: "GenerateAppPassword with not empty state", + prevStateJSON: string(dummyDataJSON), + expectedState: concatMaps(map[string]map[string]*apppb.AppPassword{ + userTest.GetId().String(): { + token: { + Password: token, + TokenScope: nil, + Label: "label", + User: userTest.GetId(), + Expiration: nil, + Ctime: now, + Utime: now, + }, + }}, + dummyData), + }, + } + + for _, test := range testCases { + t.Run(test.description, func(t *testing.T) { + // initialize temp file with `prevStateJSON` content + tmpFile := createTempFile(t, tempDir, "test.json") + defer tmpFile.Close() + fill(t, tmpFile, test.prevStateJSON) + manager, err := New(map[string]interface{}{ + "file": tmpFile.Name(), + "token_strength": len(token), + }) + if err != nil { + t.Fatal("error creating manager:", err) + } + + pw, err := manager.GenerateAppPassword(ctx, nil, "label", nil) + if err != nil { + t.Fatal("error generating password:", err) + } + + // test state in memory + + if !reflect.DeepEqual(pw, test.expectedState[userTest.GetId().String()][token]) { + t.Fatalf("apppassword differ: expected=%v got=%v", render.AsCode(test.expectedState[userTest.GetId().String()][token]), render.AsCode(pw)) + } + + if !reflect.DeepEqual(manager.(*jsonManager).passwords, test.expectedState) { + t.Fatalf("manager state differ: expected=%v got=%v", render.AsCode(test.expectedState), render.AsCode(manager.(*jsonManager).passwords)) + } + + // test saved json + + _, err = tmpFile.Seek(0, 0) + if err != nil { + t.Fatal(err) + } + data, err := ioutil.ReadAll(tmpFile) + if err != nil { + t.Fatalf("error reading file %s: %v", tmpFile.Name(), err) + } + + var jsonState map[string]map[string]*apppb.AppPassword + err = json.Unmarshal(data, &jsonState) + if err != nil { + t.Fatalf("error decoding json: %v", err) + } + + if !reflect.DeepEqual(jsonState, test.expectedState) { + t.Fatalf("json state differ: expected=%v got=%v", render.AsCode(jsonState), render.AsCode(test.expectedState)) + } + + }) + } + +} + +func TestListAppPasswords(t *testing.T) { + user0Test := &userpb.User{Id: &userpb.UserId{Idp: "0"}} + user1Test := &userpb.User{Id: &userpb.UserId{Idp: "1"}} + ctx := user.ContextSetUser(context.Background(), user0Test) + tempDir := createTempDir(t, "jsonappauth_test") + defer os.RemoveAll(tempDir) + + nowFixed := time.Date(2021, time.May, 21, 12, 21, 0, 0, time.UTC) + patchNow := monkey.Patch(time.Now, func() time.Time { return nowFixed }) + defer patchNow.Unpatch() + now := now() + + token := "1234" + + dummyDataUser0 := map[string]map[string]*apppb.AppPassword{ + user0Test.GetId().String(): { + token: { + Password: token, + TokenScope: nil, + Label: "label", + User: user0Test.GetId(), + Expiration: nil, + Ctime: now, + Utime: now, + }, + }} + + dummyDataUserExpired := map[string]map[string]*apppb.AppPassword{ + user0Test.GetId().String(): { + token: { + Password: token, + TokenScope: nil, + Label: "label", + User: user0Test.GetId(), + Expiration: &typespb.Timestamp{ + Seconds: 100, + }, + Ctime: now, + Utime: now, + }, + }} + + dummyDataUser0JSON, _ := json.Marshal(dummyDataUser0) + dummyDataUserExpiredJSON, _ := json.Marshal(dummyDataUserExpired) + + dummyDataUser1 := map[string]map[string]*apppb.AppPassword{ + user1Test.GetId().String(): { + "XXXX": { + Password: "XXXX", + TokenScope: nil, + Label: "label", + User: user1Test.GetId(), + Expiration: nil, + Ctime: now, + Utime: now, + }, + }} + + dummyDataTwoUsersJSON, _ := json.Marshal(concatMaps(dummyDataUser0, dummyDataUser1)) + + testCases := []struct { + description string + stateJSON string + expectedState []*apppb.AppPassword + }{ + { + description: "ListAppPasswords with empty state", + stateJSON: `{}`, + expectedState: make([]*apppb.AppPassword, 0), + }, + { + description: "ListAppPasswords with not json state file", + stateJSON: "", + expectedState: make([]*apppb.AppPassword, 0), + }, + { + description: "ListAppPasswords with not empty state (only one user)", + stateJSON: string(dummyDataUser0JSON), + expectedState: []*apppb.AppPassword{ + dummyDataUser0[user0Test.GetId().String()][token], + }, + }, + { + description: "ListAppPasswords with not empty state with expired password (only one user)", + stateJSON: string(dummyDataUserExpiredJSON), + expectedState: []*apppb.AppPassword{ + dummyDataUserExpired[user0Test.GetId().String()][token], + }, + }, + { + description: "ListAppPasswords with not empty state (different users)", + stateJSON: string(dummyDataTwoUsersJSON), + expectedState: []*apppb.AppPassword{ + dummyDataUser0[user0Test.GetId().String()][token], + }, + }, + } + + for _, test := range testCases { + t.Run(test.description, func(t *testing.T) { + // initialize temp file with `state_json` content + tmpFile := createTempFile(t, tempDir, "test.json") + defer tmpFile.Close() + if test.stateJSON != "" { + fill(t, tmpFile, test.stateJSON) + } + manager, err := New(map[string]interface{}{ + "file": tmpFile.Name(), + "token_strength": len(token), + }) + if err != nil { + t.Fatal("error creating manager:", err) + } + + pws, err := manager.ListAppPasswords(ctx) + if err != nil { + t.Fatal("error listing passwords:", err) + } + + if !reflect.DeepEqual(pws, test.expectedState) { + t.Fatalf("list passwords differ: expected=%v got=%v", test.expectedState, pws) + } + + }) + } + +} + +func TestInvalidateAppPassword(t *testing.T) { + userTest := &userpb.User{Id: &userpb.UserId{Idp: "0"}} + ctx := user.ContextSetUser(context.Background(), userTest) + tempDir := createTempDir(t, "jsonappauth_test") + defer os.RemoveAll(tempDir) + + nowFixed := time.Date(2021, time.May, 21, 12, 21, 0, 0, time.UTC) + patchNow := monkey.Patch(time.Now, func() time.Time { return nowFixed }) + now := now() + defer patchNow.Unpatch() + + token := "1234" + + dummyDataUser1Token := map[string]map[string]*apppb.AppPassword{ + userTest.GetId().String(): { + token: { + Password: token, + TokenScope: nil, + Label: "label", + User: userTest.GetId(), + Expiration: nil, + Ctime: now, + Utime: now, + }, + }} + + dummyDataUser1TokenJSON, _ := json.Marshal(dummyDataUser1Token) + + dummyDataUser2Token := map[string]map[string]*apppb.AppPassword{ + userTest.GetId().String(): { + token: { + Password: token, + TokenScope: nil, + Label: "label", + User: userTest.GetId(), + Expiration: nil, + Ctime: now, + Utime: now, + }, + "XXXX": { + Password: "XXXX", + TokenScope: nil, + Label: "label", + User: userTest.GetId(), + Expiration: nil, + Ctime: now, + Utime: now, + }, + }} + + dummyDataUser2TokenJSON, _ := json.Marshal(dummyDataUser2Token) + + testCases := []struct { + description string + stateJSON string + password string + expectedState map[string]map[string]*apppb.AppPassword + }{ + { + description: "InvalidateAppPassword with empty state", + stateJSON: `{}`, + password: "TOKEN_NOT_EXISTS", + expectedState: nil, + }, + { + description: "InvalidateAppPassword with not empty state and token does not exist", + stateJSON: string(dummyDataUser1TokenJSON), + password: "TOKEN_NOT_EXISTS", + expectedState: nil, + }, + { + description: "InvalidateAppPassword with not empty state and token exists", + stateJSON: string(dummyDataUser1TokenJSON), + password: token, + expectedState: map[string]map[string]*apppb.AppPassword{}, + }, + { + description: "InvalidateAppPassword with user that has more than 1 token", + stateJSON: string(dummyDataUser2TokenJSON), + password: token, + expectedState: map[string]map[string]*apppb.AppPassword{ + userTest.GetId().String(): { + "XXXX": { + Password: "XXXX", + TokenScope: nil, + Label: "label", + User: userTest.GetId(), + Expiration: nil, + Ctime: now, + Utime: now, + }, + }, + }, + }, + } + + for _, test := range testCases { + t.Run(test.description, func(t *testing.T) { + // initialize temp file with `state_json` content + tmpFile := createTempFile(t, tempDir, "test.json") + fill(t, tmpFile, test.stateJSON) + manager, err := New(map[string]interface{}{ + "file": tmpFile.Name(), + "token_strength": 4, + }) + if err != nil { + t.Fatal("error creating manager:", err) + } + + err = manager.InvalidateAppPassword(ctx, test.password) + if test.expectedState == nil { + if err == nil { + t.Fatalf("no error (but we expected one) while get manager") + } else { + t.Skip() + } + } + if !reflect.DeepEqual(test.expectedState, manager.(*jsonManager).passwords) { + t.Fatalf("apppauth state differ: expected=%v got=%v", render.AsCode(test.expectedState), render.AsCode(manager.(*jsonManager).passwords)) + } + + }) + } + +} + +func TestGetAppPassword(t *testing.T) { + userTest := &userpb.User{Id: &userpb.UserId{Idp: "0"}} + ctx := user.ContextSetUser(context.Background(), userTest) + tempDir := createTempDir(t, "jsonappauth_test") + defer os.RemoveAll(tempDir) + + nowFixed := time.Date(2021, time.May, 21, 12, 21, 0, 0, time.UTC) + patchNow := monkey.Patch(time.Now, func() time.Time { return nowFixed }) + defer patchNow.Unpatch() + + now := now() + token := "1234" + + dummyDataUser1Token := map[string]map[string]*apppb.AppPassword{ + userTest.GetId().String(): { + token: { + Password: token, + TokenScope: nil, + Label: "label", + User: userTest.GetId(), + Expiration: nil, + Ctime: now, + Utime: now, + }, + }} + + dummyDataUserExpired := map[string]map[string]*apppb.AppPassword{ + userTest.GetId().String(): { + token: { + Password: token, + TokenScope: nil, + Label: "label", + User: userTest.GetId(), + Expiration: &typespb.Timestamp{ + Seconds: 100, + }, + Ctime: now, + Utime: now, + }, + }} + + dummyDataUserFutureExpiration := map[string]map[string]*apppb.AppPassword{ + userTest.GetId().String(): { + token: { + Password: token, + TokenScope: nil, + Label: "label", + User: userTest.GetId(), + Expiration: &typespb.Timestamp{ + Seconds: uint64(time.Now().Unix()) + 3600, + }, + Ctime: now, + Utime: now, + }, + }} + + dummyDataUser1TokenJSON, _ := json.Marshal(dummyDataUser1Token) + dummyDataUserExpiredJSON, _ := json.Marshal(dummyDataUserExpired) + dummyDataUserFutureExpirationJSON, _ := json.Marshal(dummyDataUserFutureExpiration) + + dummyDataDifferentUserToken := map[string]map[string]*apppb.AppPassword{ + "OTHER_USER_ID": { + token: { + Password: token, + TokenScope: nil, + Label: "label", + User: &userpb.UserId{Idp: "OTHER_USER_ID"}, + Expiration: nil, + Ctime: now, + Utime: now, + }, + }} + + dummyDataDifferentUserTokenJSON, _ := json.Marshal(dummyDataDifferentUserToken) + + testCases := []struct { + description string + stateJSON string + password string + expectedState *apppb.AppPassword + }{ + { + description: "GetAppPassword with token that does not exist", + stateJSON: string(dummyDataUser1TokenJSON), + password: "TOKEN_NOT_EXISTS", + expectedState: nil, + }, + { + description: "GetAppPassword with expired token", + stateJSON: string(dummyDataUserExpiredJSON), + password: "TOKEN_NOT_EXISTS", + expectedState: nil, + }, + { + description: "GetAppPassword with token with expiration set in the future", + stateJSON: string(dummyDataUserFutureExpirationJSON), + password: "1234", + expectedState: dummyDataUserFutureExpiration[userTest.GetId().String()][token], + }, + { + description: "GetAppPassword with token that exists but different user", + stateJSON: string(dummyDataDifferentUserTokenJSON), + password: "1234", + expectedState: nil, + }, + { + description: "GetAppPassword with token that exists owned by user", + stateJSON: string(dummyDataUser1TokenJSON), + password: "1234", + expectedState: dummyDataUser1Token[userTest.GetId().String()][token], + }, + } + + for _, test := range testCases { + t.Run(test.description, func(t *testing.T) { + // initialize temp file with `state_json` content + tmpFile := createTempFile(t, tempDir, "test.json") + fill(t, tmpFile, test.stateJSON) + manager, err := New(map[string]interface{}{ + "file": tmpFile.Name(), + "token_strength": 4, + }) + if err != nil { + t.Fatal("error creating manager:", err) + } + + pw, err := manager.GetAppPassword(ctx, userTest.GetId(), test.password) + if test.expectedState == nil { + if err == nil { + t.Fatalf("no error (but we expected one) while get manager") + } else { + t.Skip() + } + } + if !reflect.DeepEqual(test.expectedState, pw) { + t.Fatalf("apppauth state differ: expected=%v got=%v", render.AsCode(test.expectedState), render.AsCode(pw)) + } + + }) + } +} + +func createTempDir(t *testing.T, name string) string { + tempDir, err := ioutil.TempDir("", name) + if err != nil { + t.Fatalf("error while creating temp dir: %v", err) + } + return tempDir +} + +func createTempFile(t *testing.T, tempDir string, name string) *os.File { + tempFile, err := ioutil.TempFile(tempDir, name) + if err != nil { + t.Fatalf("error while creating temp file: %v", err) + } + return tempFile +} + +func fill(t *testing.T, file *os.File, data string) { + _, err := file.WriteString(data) + if err != nil { + t.Fatalf("error while writing to file: %v", err) + } +} + +func concatMaps(maps ...map[string]map[string]*apppb.AppPassword) map[string]map[string]*apppb.AppPassword { + res := make(map[string]map[string]*apppb.AppPassword) + for _, m := range maps { + for k := range m { + res[k] = m[k] + } + } + return res +} diff --git a/pkg/appauth/manager/loader/loader.go b/pkg/appauth/manager/loader/loader.go new file mode 100644 index 0000000000..ffe3ce1a18 --- /dev/null +++ b/pkg/appauth/manager/loader/loader.go @@ -0,0 +1,25 @@ +// Copyright 2018-2021 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package loader + +import ( + // Load core application auth manager drivers. + _ "github.com/cs3org/reva/pkg/appauth/manager/json" + // Add your own here +) diff --git a/pkg/appauth/manager/registry/registry.go b/pkg/appauth/manager/registry/registry.go new file mode 100644 index 0000000000..340eb9b433 --- /dev/null +++ b/pkg/appauth/manager/registry/registry.go @@ -0,0 +1,34 @@ +// Copyright 2018-2021 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package registry + +import "github.com/cs3org/reva/pkg/appauth" + +// NewFunc is the function that application auth implementations +// should register at init time. +type NewFunc func(map[string]interface{}) (appauth.Manager, error) + +// NewFuncs is a map containing all the registered application auth managers. +var NewFuncs = map[string]NewFunc{} + +// Register registers a new application auth manager new function. +// Not safe for concurrent use. Safe for use from package init. +func Register(name string, f NewFunc) { + NewFuncs[name] = f +} diff --git a/pkg/auth/manager/appauth/appauth.go b/pkg/auth/manager/appauth/appauth.go new file mode 100644 index 0000000000..cb4b29a020 --- /dev/null +++ b/pkg/auth/manager/appauth/appauth.go @@ -0,0 +1,91 @@ +// Copyright 2018-2021 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package appauth + +import ( + "context" + + appauthpb "github.com/cs3org/go-cs3apis/cs3/auth/applications/v1beta1" + authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1" + user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + "github.com/cs3org/reva/pkg/auth" + "github.com/cs3org/reva/pkg/auth/manager/registry" + "github.com/cs3org/reva/pkg/errtypes" + "github.com/cs3org/reva/pkg/rgrpc/todo/pool" + "github.com/mitchellh/mapstructure" + "github.com/pkg/errors" +) + +func init() { + registry.Register("appauth", New) +} + +type manager struct { + GatewayAddr string `mapstructure:"gateway_addr"` +} + +// New returns a new auth Manager. +func New(m map[string]interface{}) (auth.Manager, error) { + mgr := &manager{} + err := mapstructure.Decode(m, mgr) + if err != nil { + return nil, errors.Wrap(err, "error decoding conf") + } + return mgr, nil +} + +func (m *manager) Authenticate(ctx context.Context, username, password string) (*user.User, map[string]*authpb.Scope, error) { + gtw, err := pool.GetGatewayServiceClient(m.GatewayAddr) + if err != nil { + return nil, nil, err + } + + // get user info + userResponse, err := gtw.GetUserByClaim(ctx, &user.GetUserByClaimRequest{ + Claim: "username", + Value: username, + }) + + switch { + case err != nil: + return nil, nil, err + case userResponse.Status.Code == rpcv1beta1.Code_CODE_NOT_FOUND: + return nil, nil, errtypes.NotFound(userResponse.Status.Message) + case userResponse.Status.Code != rpcv1beta1.Code_CODE_OK: + return nil, nil, errtypes.InternalError(userResponse.Status.Message) + } + + // get the app password associated with the user and password + appAuthResponse, err := gtw.GetAppPassword(ctx, &appauthpb.GetAppPasswordRequest{ + User: userResponse.GetUser().Id, + Password: password, + }) + + switch { + case err != nil: + return nil, nil, err + case appAuthResponse.Status.Code == rpcv1beta1.Code_CODE_NOT_FOUND: + return nil, nil, errtypes.NotFound(appAuthResponse.Status.Message) + case appAuthResponse.Status.Code != rpcv1beta1.Code_CODE_OK: + return nil, nil, errtypes.InternalError(appAuthResponse.Status.Message) + } + + return userResponse.GetUser(), appAuthResponse.GetAppPassword().TokenScope, nil +} diff --git a/pkg/auth/manager/publicshares/publicshares.go b/pkg/auth/manager/publicshares/publicshares.go index 3dc074f1d3..0b2df90ee7 100644 --- a/pkg/auth/manager/publicshares/publicshares.go +++ b/pkg/auth/manager/publicshares/publicshares.go @@ -31,9 +31,9 @@ import ( types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/cs3org/reva/pkg/auth" "github.com/cs3org/reva/pkg/auth/manager/registry" + "github.com/cs3org/reva/pkg/auth/scope" "github.com/cs3org/reva/pkg/errtypes" "github.com/cs3org/reva/pkg/rgrpc/todo/pool" - "github.com/cs3org/reva/pkg/utils" "github.com/mitchellh/mapstructure" "github.com/pkg/errors" ) @@ -127,33 +127,17 @@ func (m *manager) Authenticate(ctx context.Context, token, secret string) (*user return nil, nil, err } - scope, err := m.getScope(ctx, publicShareResponse.GetShare()) - if err != nil { - return nil, nil, err - } - - return getUserResponse.GetUser(), scope, nil -} - -func (m *manager) getScope(ctx context.Context, share *link.PublicShare) (map[string]*authpb.Scope, error) { + share := publicShareResponse.GetShare() role := authpb.Role_ROLE_VIEWER if share.Permissions.Permissions.InitiateFileUpload { role = authpb.Role_ROLE_EDITOR } - - val, err := utils.MarshalProtoV1ToJSON(share) + scope, err := scope.GetPublicShareScope(share, role) if err != nil { - return nil, err + return nil, nil, err } - return map[string]*authpb.Scope{ - "publicshare": &authpb.Scope{ - Resource: &types.OpaqueEntry{ - Decoder: "json", - Value: val, - }, - Role: role, - }, - }, nil + + return getUserResponse.GetUser(), scope, nil } // ErrPasswordNotProvided is returned when the public share is password protected, but there was no password on the request diff --git a/pkg/auth/scope/publicshare.go b/pkg/auth/scope/publicshare.go index b88f48c78a..1b7b686631 100644 --- a/pkg/auth/scope/publicshare.go +++ b/pkg/auth/scope/publicshare.go @@ -26,6 +26,7 @@ import ( link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" registry "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1" + types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/cs3org/reva/pkg/errtypes" "github.com/cs3org/reva/pkg/utils" ) @@ -63,7 +64,7 @@ func publicshareScope(scope *authpb.Scope, resource interface{}) (bool, error) { case *link.GetPublicShareRequest: return checkPublicShareRef(&share, v.GetRef()), nil case string: - return checkPath(&share, v), nil + return checkPath(v), nil } return false, errtypes.InternalError(fmt.Sprintf("resource type assertion failed: %+v", resource)) @@ -86,15 +87,20 @@ func checkPublicShareRef(s *link.PublicShare, ref *link.PublicShareReference) bo return ref.GetToken() == s.Token } -func checkPath(s *link.PublicShare, path string) bool { - paths := []string{ - "/dataprovider", - "/data", - } - for _, p := range paths { - if strings.HasPrefix(path, p) { - return true - } +// GetPublicShareScope returns the scope to allow access to a public share and +// the shared resource. +func GetPublicShareScope(share *link.PublicShare, role authpb.Role) (map[string]*authpb.Scope, error) { + val, err := utils.MarshalProtoV1ToJSON(share) + if err != nil { + return nil, err } - return false + return map[string]*authpb.Scope{ + "publicshare:" + share.Id.OpaqueId: &authpb.Scope{ + Resource: &types.OpaqueEntry{ + Decoder: "json", + Value: val, + }, + Role: role, + }, + }, nil } diff --git a/pkg/auth/scope/resourceinfo.go b/pkg/auth/scope/resourceinfo.go new file mode 100644 index 0000000000..3d75938bce --- /dev/null +++ b/pkg/auth/scope/resourceinfo.go @@ -0,0 +1,110 @@ +// Copyright 2018-2021 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package scope + +import ( + "fmt" + "strings" + + authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1" + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + registry "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1" + types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + "github.com/cs3org/reva/pkg/errtypes" + "github.com/cs3org/reva/pkg/utils" +) + +func resourceinfoScope(scope *authpb.Scope, resource interface{}) (bool, error) { + var r provider.ResourceInfo + err := utils.UnmarshalJSONToProtoV1(scope.Resource.Value, &r) + if err != nil { + return false, err + } + + switch v := resource.(type) { + // Viewer role + case *registry.GetStorageProvidersRequest: + return checkResourceInfo(&r, v.GetRef()), nil + case *provider.StatRequest: + return checkResourceInfo(&r, v.GetRef()), nil + case *provider.ListContainerRequest: + return checkResourceInfo(&r, v.GetRef()), nil + case *provider.InitiateFileDownloadRequest: + return checkResourceInfo(&r, v.GetRef()), nil + + // Editor role + // TODO(ishank011): Add role checks, + // need to return appropriate status codes in the ocs/ocdav layers. + case *provider.CreateContainerRequest: + return checkResourceInfo(&r, v.GetRef()), nil + case *provider.DeleteRequest: + return checkResourceInfo(&r, v.GetRef()), nil + case *provider.MoveRequest: + return checkResourceInfo(&r, v.GetSource()) && checkResourceInfo(&r, v.GetDestination()), nil + case *provider.InitiateFileUploadRequest: + return checkResourceInfo(&r, v.GetRef()), nil + + case string: + return checkPath(v), nil + } + + return false, errtypes.InternalError(fmt.Sprintf("resource type assertion failed: %+v", resource)) +} + +func checkResourceInfo(inf *provider.ResourceInfo, ref *provider.Reference) bool { + // ref: > + if ref.GetId() != nil { + return inf.Id.StorageId == ref.GetId().StorageId && inf.Id.OpaqueId == ref.GetId().OpaqueId + } + // ref: + if strings.HasPrefix(ref.GetPath(), inf.Path) { + return true + } + return false +} + +func checkPath(path string) bool { + paths := []string{ + "/dataprovider", + "/data", + } + for _, p := range paths { + if strings.HasPrefix(path, p) { + return true + } + } + return false +} + +// GetResourceInfoScope returns the scope to allow access to a resource info object. +func GetResourceInfoScope(r *provider.ResourceInfo, role authpb.Role) (map[string]*authpb.Scope, error) { + val, err := utils.MarshalProtoV1ToJSON(r) + if err != nil { + return nil, err + } + return map[string]*authpb.Scope{ + "resourceinfo:" + r.Id.String(): &authpb.Scope{ + Resource: &types.OpaqueEntry{ + Decoder: "json", + Value: val, + }, + Role: role, + }, + }, nil +} diff --git a/pkg/auth/scope/scope.go b/pkg/auth/scope/scope.go index 42ba65c11f..35bdb29cfc 100644 --- a/pkg/auth/scope/scope.go +++ b/pkg/auth/scope/scope.go @@ -19,54 +19,35 @@ package scope import ( + "strings" + authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1" - provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" - types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" - "github.com/cs3org/reva/pkg/utils" ) // Verifier is the function signature which every scope verifier should implement. type Verifier func(*authpb.Scope, interface{}) (bool, error) var supportedScopes = map[string]Verifier{ - "user": userScope, - "publicshare": publicshareScope, + "user": userScope, + "publicshare": publicshareScope, + "resourceinfo": resourceinfoScope, } // VerifyScope is the function to be called when dismantling tokens to check if // the token has access to a particular resource. func VerifyScope(scopeMap map[string]*authpb.Scope, resource interface{}) (bool, error) { for k, scope := range scopeMap { - verifierFunc := supportedScopes[k] - valid, err := verifierFunc(scope, resource) - if err != nil { - continue - } - if valid { - return true, nil + for s, f := range supportedScopes { + if strings.HasPrefix(k, s) { + valid, err := f(scope, resource) + if err != nil { + continue + } + if valid { + return true, nil + } + } } } return false, nil } - -// GetOwnerScope returns the default owner scope with access to all resources. -func GetOwnerScope() (map[string]*authpb.Scope, error) { - ref := &provider.Reference{ - Spec: &provider.Reference_Path{ - Path: "/", - }, - } - val, err := utils.MarshalProtoV1ToJSON(ref) - if err != nil { - return nil, err - } - return map[string]*authpb.Scope{ - "user": &authpb.Scope{ - Resource: &types.OpaqueEntry{ - Decoder: "json", - Value: val, - }, - Role: authpb.Role_ROLE_OWNER, - }, - }, nil -} diff --git a/pkg/auth/scope/user.go b/pkg/auth/scope/user.go index f885a43ba8..42ebf47728 100644 --- a/pkg/auth/scope/user.go +++ b/pkg/auth/scope/user.go @@ -18,10 +18,37 @@ package scope -import authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1" +import ( + authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1" + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + "github.com/cs3org/reva/pkg/utils" +) func userScope(scope *authpb.Scope, resource interface{}) (bool, error) { // Always return true. Registered users can access all paths. // TODO(ishank011): Add checks for read/write permissions. return true, nil } + +// GetOwnerScope returns the default owner scope with access to all resources. +func GetOwnerScope() (map[string]*authpb.Scope, error) { + ref := &provider.Reference{ + Spec: &provider.Reference_Path{ + Path: "/", + }, + } + val, err := utils.MarshalProtoV1ToJSON(ref) + if err != nil { + return nil, err + } + return map[string]*authpb.Scope{ + "user": &authpb.Scope{ + Resource: &types.OpaqueEntry{ + Decoder: "json", + Value: val, + }, + Role: authpb.Role_ROLE_OWNER, + }, + }, nil +} diff --git a/pkg/rgrpc/todo/pool/pool.go b/pkg/rgrpc/todo/pool/pool.go index 4c0efb4471..e4fac1866b 100644 --- a/pkg/rgrpc/todo/pool/pool.go +++ b/pkg/rgrpc/todo/pool/pool.go @@ -23,6 +23,7 @@ import ( appprovider "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1" appregistry "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1" + applicationauth "github.com/cs3org/go-cs3apis/cs3/auth/applications/v1beta1" authprovider "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1" authregistry "github.com/cs3org/go-cs3apis/cs3/auth/registry/v1beta1" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" @@ -59,6 +60,7 @@ func newProvider() provider { var ( storageProviders = newProvider() authProviders = newProvider() + appAuthProviders = newProvider() authRegistries = newProvider() userShareProviders = newProvider() ocmShareProviders = newProvider() @@ -206,6 +208,25 @@ func GetAuthProviderServiceClient(endpoint string) (authprovider.ProviderAPIClie return v, nil } +// GetAppAuthProviderServiceClient returns a new AppAuthProviderServiceClient. +func GetAppAuthProviderServiceClient(endpoint string) (applicationauth.ApplicationsAPIClient, error) { + appAuthProviders.m.Lock() + defer appAuthProviders.m.Unlock() + + if c, ok := appAuthProviders.conn[endpoint]; ok { + return c.(applicationauth.ApplicationsAPIClient), nil + } + + conn, err := NewConn(endpoint) + if err != nil { + return nil, err + } + + v := applicationauth.NewApplicationsAPIClient(conn) + appAuthProviders.conn[endpoint] = v + return v, nil +} + // GetUserShareProviderClient returns a new UserShareProviderClient. func GetUserShareProviderClient(endpoint string) (collaboration.CollaborationAPIClient, error) { userShareProviders.m.Lock()