diff --git a/changelog/unreleased/app-passwords-cli.md b/changelog/unreleased/app-passwords-cli.md new file mode 100644 index 0000000000..6211cc0d80 --- /dev/null +++ b/changelog/unreleased/app-passwords-cli.md @@ -0,0 +1,6 @@ +Enhancement: Application passwords CLI + +This PR adds the CLI commands `token-list`, `token-create` and `token-remove` +to manage tokens with limited scope on behalf of registered users. + +https://github.com/cs3org/reva/pull/1719 diff --git a/cmd/reva/app-tokens-create.go b/cmd/reva/app-tokens-create.go new file mode 100644 index 0000000000..f23295f5e7 --- /dev/null +++ b/cmd/reva/app-tokens-create.go @@ -0,0 +1,251 @@ +// 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 main + +import ( + "context" + "io" + "strings" + "time" + + authapp "github.com/cs3org/go-cs3apis/cs3/auth/applications/v1beta1" + authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1" + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + share "github.com/cs3org/go-cs3apis/cs3/sharing/link/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/auth/scope" + "github.com/cs3org/reva/pkg/errtypes" +) + +type appTokenCreateOpts struct { + Expiration string + Label string + Path stringSlice + Share stringSlice + Unlimited bool +} + +type stringSlice []string + +func (ss *stringSlice) Set(value string) error { + *ss = append(*ss, value) + return nil +} + +func (ss *stringSlice) String() string { + return strings.Join([]string(*ss), ",") +} + +const layoutTime = "2006-01-02" + +func appTokensCreateCommand() *command { + cmd := newCommand("app-tokens-create") + cmd.Description = func() string { return "create a new application tokens" } + cmd.Usage = func() string { return "Usage: token-create" } + + var path, share stringSlice + label := cmd.String("label", "", "set a label") + expiration := cmd.String("expiration", "", "set expiration time (format )") + cmd.Var(&path, "path", "create a token for a file (format path:[r|w]). It is possible specify this flag multiple times") + cmd.Var(&share, "share", "create a token for a share (format shareid:[r|w]). It is possible specify this flag multiple times") + unlimited := cmd.Bool("all", false, "create a token with an unlimited scope") + + cmd.ResetFlags = func() { + path, share, label, expiration, unlimited = nil, nil, nil, nil, nil + } + + cmd.Action = func(w ...io.Writer) error { + + createOpts := &appTokenCreateOpts{ + Expiration: *expiration, + Label: *label, + Path: path, + Share: share, + Unlimited: *unlimited, + } + + err := checkOpts(createOpts) + if err != nil { + return err + } + + client, err := getClient() + if err != nil { + return err + } + + ctx := getAuthContext() + + scope, err := getScope(ctx, client, createOpts) + if err != nil { + return err + } + + // parse eventually expiration time + var expiration *types.Timestamp + if createOpts.Expiration != "" { + exp, err := time.Parse(layoutTime, createOpts.Expiration) + if err != nil { + return err + } + expiration = &types.Timestamp{ + Seconds: uint64(exp.Unix()), + } + } + + generateAppPasswordResponse, err := client.GenerateAppPassword(ctx, &authapp.GenerateAppPasswordRequest{ + Expiration: expiration, + Label: createOpts.Label, + TokenScope: scope, + }) + + if err != nil { + return err + } + if generateAppPasswordResponse.Status.Code != rpc.Code_CODE_OK { + return formatError(generateAppPasswordResponse.Status) + } + + err = printTableAppPasswords([]*authapp.AppPassword{generateAppPasswordResponse.AppPassword}) + if err != nil { + return err + } + + return nil + } + + return cmd +} + +func getScope(ctx context.Context, client gateway.GatewayAPIClient, opts *appTokenCreateOpts) (map[string]*authpb.Scope, error) { + var scopeList []map[string]*authpb.Scope + switch { + case opts.Unlimited: + return scope.GetOwnerScope() + case len(opts.Share) != 0: + // TODO(gmgigi96): verify format + for _, entry := range opts.Share { + // share = xxxx:[r|w] + shareIDPerm := strings.Split(entry, ":") + shareID, perm := shareIDPerm[0], shareIDPerm[1] + scope, err := getPublicShareScope(ctx, client, shareID, perm) + if err != nil { + return nil, err + } + scopeList = append(scopeList, scope) + } + fallthrough + case len(opts.Path) != 0: + // TODO(gmgigi96): verify format + for _, entry := range opts.Path { + // path = /home/a/b:[r|w] + pathPerm := strings.Split(entry, ":") + path, perm := pathPerm[0], pathPerm[1] + scope, err := getPathScope(ctx, client, path, perm) + if err != nil { + return nil, err + } + scopeList = append(scopeList, scope) + } + fallthrough + default: + return mergeListScopeIntoMap(scopeList), nil + } +} + +func mergeListScopeIntoMap(scopeList []map[string]*authpb.Scope) map[string]*authpb.Scope { + merged := make(map[string]*authpb.Scope) + for _, scope := range scopeList { + for k, v := range scope { + merged[k] = v + } + } + return merged +} + +func getPublicShareScope(ctx context.Context, client gateway.GatewayAPIClient, shareID, perm string) (map[string]*authpb.Scope, error) { + role, err := parsePermission(perm) + if err != nil { + return nil, err + } + + publicShareResponse, err := client.GetPublicShare(ctx, &share.GetPublicShareRequest{ + Ref: &share.PublicShareReference{ + Spec: &share.PublicShareReference_Id{ + Id: &share.PublicShareId{ + OpaqueId: shareID, + }, + }, + }, + }) + + if err != nil { + return nil, err + } + if publicShareResponse.Status.Code != rpc.Code_CODE_OK { + return nil, formatError(publicShareResponse.Status) + } + + return scope.GetPublicShareScope(publicShareResponse.GetShare(), role) +} + +func getPathScope(ctx context.Context, client gateway.GatewayAPIClient, path, perm string) (map[string]*authpb.Scope, error) { + role, err := parsePermission(perm) + if err != nil { + return nil, err + } + + statResponse, err := client.Stat(ctx, &provider.StatRequest{ + Ref: &provider.Reference{ + Spec: &provider.Reference_Path{ + Path: path, + }, + }, + }) + + if err != nil { + return nil, err + } + if statResponse.Status.Code != rpc.Code_CODE_OK { + return nil, formatError(statResponse.Status) + } + + return scope.GetResourceInfoScope(statResponse.GetInfo(), role) +} + +// parse permission string in the form of "rw" to create a role +func parsePermission(perm string) (authpb.Role, error) { + switch perm { + case "r": + return authpb.Role_ROLE_VIEWER, nil + case "w": + return authpb.Role_ROLE_EDITOR, nil + default: + return authpb.Role_ROLE_INVALID, errtypes.BadRequest("not recognised permission") + } +} + +func checkOpts(opts *appTokenCreateOpts) error { + if len(opts.Share) == 0 && len(opts.Path) == 0 && !opts.Unlimited { + return errtypes.BadRequest("specify a token scope") + } + return nil +} diff --git a/cmd/reva/app-tokens-list.go b/cmd/reva/app-tokens-list.go new file mode 100644 index 0000000000..62605d434f --- /dev/null +++ b/cmd/reva/app-tokens-list.go @@ -0,0 +1,106 @@ +// 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 main + +import ( + "io" + "os" + "strings" + "time" + + applications "github.com/cs3org/go-cs3apis/cs3/auth/applications/v1beta1" + authpv "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1" + rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + scope "github.com/cs3org/reva/pkg/auth/scope" + "github.com/jedib0t/go-pretty/table" +) + +func appTokensListCommand() *command { + cmd := newCommand("app-tokens-list") + cmd.Description = func() string { return "list all the application tokens" } + cmd.Usage = func() string { return "Usage: token-list" } + + cmd.Action = func(w ...io.Writer) error { + + client, err := getClient() + if err != nil { + return err + } + + ctx := getAuthContext() + listResponse, err := client.ListAppPasswords(ctx, &applications.ListAppPasswordsRequest{}) + + if err != nil { + return err + } + + if listResponse.Status.Code != rpc.Code_CODE_OK { + return formatError(listResponse.Status) + } + + err = printTableAppPasswords(listResponse.AppPasswords) + if err != nil { + return err + } + + return nil + } + return cmd +} + +func printTableAppPasswords(listPw []*applications.AppPassword) error { + header := table.Row{"Token", "Scope", "Label", "Expiration", "Creation Time", "Last Used Time"} + + t := table.NewWriter() + t.SetOutputMirror(os.Stdout) + + t.AppendHeader(header) + + for _, pw := range listPw { + scopeFormatted, err := prettyFormatScope(pw.TokenScope) + if err != nil { + return err + } + t.AppendRow(table.Row{pw.Password, scopeFormatted, pw.Label, formatTime(pw.Expiration), formatTime(pw.Ctime), formatTime(pw.Utime)}) + } + + t.Render() + return nil +} + +func formatTime(t *types.Timestamp) string { + if t == nil { + return "" + } + return time.Unix(int64(t.Seconds), 0).String() +} + +func prettyFormatScope(scopeMap map[string]*authpv.Scope) (string, error) { + var scopeFormatted strings.Builder + for scType, sc := range scopeMap { + scopeStr, err := scope.FormatScope(scType, sc) + if err != nil { + return "", err + } + scopeFormatted.WriteString(scopeStr) + scopeFormatted.WriteString(", ") + } + return scopeFormatted.String()[:scopeFormatted.Len()-2], nil +} diff --git a/cmd/reva/app-tokens-remove.go b/cmd/reva/app-tokens-remove.go new file mode 100644 index 0000000000..1fc0fcf8ea --- /dev/null +++ b/cmd/reva/app-tokens-remove.go @@ -0,0 +1,65 @@ +// 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 main + +import ( + "fmt" + "io" + + applicationsv1beta1 "github.com/cs3org/go-cs3apis/cs3/auth/applications/v1beta1" + rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + "github.com/cs3org/reva/pkg/errtypes" +) + +func appTokensRemoveCommand() *command { + cmd := newCommand("app-tokens-remove") + cmd.Description = func() string { return "remove an application token" } + cmd.Usage = func() string { return "Usage: token-remove " } + + cmd.Action = func(w ...io.Writer) error { + if cmd.NArg() != 1 { + return errtypes.BadRequest("Invalid arguments: " + cmd.Usage()) + } + + token := cmd.Arg(0) + ctx := getAuthContext() + + client, err := getClient() + if err != nil { + return err + } + + response, err := client.InvalidateAppPassword(ctx, &applicationsv1beta1.InvalidateAppPasswordRequest{ + Password: token, + }) + + if err != nil { + return err + } + + if response.Status.Code != rpc.Code_CODE_OK { + return formatError(response.Status) + } + + fmt.Println("OK") + return nil + } + + return cmd +} diff --git a/cmd/reva/main.go b/cmd/reva/main.go index ade6772e0c..5ffd2921af 100644 --- a/cmd/reva/main.go +++ b/cmd/reva/main.go @@ -84,6 +84,9 @@ var ( transferCreateCommand(), transferGetStatusCommand(), transferCancelCommand(), + appTokensListCommand(), + appTokensRemoveCommand(), + appTokensCreateCommand(), helpCommand(), } ) diff --git a/examples/storage-references/applicationauth.toml b/examples/storage-references/applicationauth.toml new file mode 100644 index 0000000000..bca8cd899e --- /dev/null +++ b/examples/storage-references/applicationauth.toml @@ -0,0 +1,8 @@ +[grpc] +address = "0.0.0.0:15000" + +[grpc.services.authprovider] +auth_manager = "appauth" + +[grpc.services.authprovider.auth_managers.appauth] +gateway_addr = "localhost:19000" \ No newline at end of file diff --git a/examples/storage-references/gateway.toml b/examples/storage-references/gateway.toml index 402935c9ee..a477512a5b 100644 --- a/examples/storage-references/gateway.toml +++ b/examples/storage-references/gateway.toml @@ -18,7 +18,9 @@ home_provider = "/home" [grpc.services.authregistry.drivers.static.rules] basic = "localhost:19000" publicshares = "localhost:16000" +appauth = "localhost:15000" +[grpc.services.applicationauth] [grpc.services.userprovider] [grpc.services.usershareprovider] [grpc.services.groupprovider] diff --git a/pkg/appauth/manager/json/json.go b/pkg/appauth/manager/json/json.go index 1449adcac9..8018fb640a 100644 --- a/pkg/appauth/manager/json/json.go +++ b/pkg/appauth/manager/json/json.go @@ -37,6 +37,7 @@ import ( "github.com/mitchellh/mapstructure" "github.com/pkg/errors" "github.com/sethvargo/go-password/password" + "golang.org/x/crypto/bcrypt" ) func init() { @@ -44,8 +45,9 @@ func init() { } type config struct { - File string `mapstructure:"file"` - TokenStrength int `mapstructure:"token_strength"` + File string `mapstructure:"file"` + TokenStrength int `mapstructure:"token_strength"` + PasswordHashCost int `mapstructure:"password_hash_cost"` } type jsonManager struct { @@ -79,6 +81,12 @@ func (c *config) init() { if c.File == "" { c.File = "/var/tmp/reva/appauth.json" } + if c.TokenStrength == 0 { + c.TokenStrength = 16 + } + if c.PasswordHashCost == 0 { + c.PasswordHashCost = 11 + } } func parseConfig(m map[string]interface{}) (*config, error) { @@ -121,15 +129,20 @@ func loadOrCreate(file string) (*jsonManager, error) { } 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) + token, err := password.Generate(mgr.config.TokenStrength, mgr.config.TokenStrength/2, 0, false, false) + if err != nil { + return nil, errors.Wrap(err, "error creating new token") + } + tokenHashed, err := bcrypt.GenerateFromPassword([]byte(token), mgr.config.PasswordHashCost) if err != nil { return nil, errors.Wrap(err, "error creating new token") } userID := user.ContextMustGetUser(ctx).GetId() ctime := now() + password := string(tokenHashed) appPass := &apppb.AppPassword{ - Password: token, + Password: password, TokenScope: scope, Label: label, Expiration: expiration, @@ -145,14 +158,16 @@ func (mgr *jsonManager) GenerateAppPassword(ctx context.Context, scope map[strin mgr.passwords[userID.String()] = make(map[string]*apppb.AppPassword) } - mgr.passwords[userID.String()][token] = appPass + mgr.passwords[userID.String()][password] = appPass err = mgr.save() if err != nil { return nil, errors.Wrap(err, "error saving new token") } - return appPass, nil + clonedAppPass := *appPass + clonedAppPass.Password = token + return &clonedAppPass, nil } func (mgr *jsonManager) ListAppPasswords(ctx context.Context) ([]*apppb.AppPassword, error) { @@ -199,20 +214,26 @@ func (mgr *jsonManager) GetAppPassword(ctx context.Context, userID *userpb.UserI 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") + for hash, pw := range appPassword { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + if err == nil { + // password found + if pw.Expiration != nil && pw.Expiration.Seconds != 0 && uint64(time.Now().Unix()) > pw.Expiration.Seconds { + // password expired + return nil, errtypes.NotFound("password not found") + } + // password not expired + // update last used time + pw.Utime = now() + if err := mgr.save(); err != nil { + return nil, errors.Wrap(err, "error saving file") + } + + return pw, nil + } } - pw.Utime = now() - if err := mgr.save(); err != nil { - return nil, errors.Wrap(err, "error saving file") - } - return pw, nil + return nil, errtypes.NotFound("password not found") } func now() *typespb.Timestamp { diff --git a/pkg/appauth/manager/json/json_test.go b/pkg/appauth/manager/json/json_test.go index e42d73f5b5..2c51251cde 100644 --- a/pkg/appauth/manager/json/json_test.go +++ b/pkg/appauth/manager/json/json_test.go @@ -19,6 +19,7 @@ package json import ( + "bytes" "context" "encoding/json" "io/ioutil" @@ -34,6 +35,7 @@ import ( "github.com/cs3org/reva/pkg/user" "github.com/gdexlab/go-render/render" "github.com/sethvargo/go-password/password" + "golang.org/x/crypto/bcrypt" ) func TestNewManager(t *testing.T) { @@ -50,10 +52,12 @@ func TestNewManager(t *testing.T) { jsonOkFile := createTempFile(t, tempDir, "ok.json") defer jsonOkFile.Close() + hashToken, _ := bcrypt.GenerateFromPassword([]byte("1234"), 10) + dummyData := map[string]map[string]*apppb.AppPassword{ userTest.GetId().String(): { - "1234": { - Password: "1234", + string(hashToken): { + Password: string(hashToken), TokenScope: nil, Label: "label", User: userTest.GetId(), @@ -86,13 +90,15 @@ func TestNewManager(t *testing.T) { { description: "New appauth manager from empty state file", configMap: map[string]interface{}{ - "file": jsonEmptyFile.Name(), - "token_strength": 10, + "file": jsonEmptyFile.Name(), + "token_strength": 10, + "password_hash_cost": 12, }, expected: &jsonManager{ config: &config{ - File: jsonEmptyFile.Name(), - TokenStrength: 10, + File: jsonEmptyFile.Name(), + TokenStrength: 10, + PasswordHashCost: 12, }, passwords: map[string]map[string]*apppb.AppPassword{}, }, @@ -100,13 +106,15 @@ func TestNewManager(t *testing.T) { { description: "New appauth manager from state file", configMap: map[string]interface{}{ - "file": jsonOkFile.Name(), - "token_strength": 10, + "file": jsonOkFile.Name(), + "token_strength": 10, + "password_hash_cost": 10, }, expected: &jsonManager{ config: &config{ - File: jsonOkFile.Name(), - TokenStrength: 10, + File: jsonOkFile.Name(), + TokenStrength: 10, + PasswordHashCost: 10, }, passwords: dummyData, }, @@ -145,10 +153,17 @@ func TestGenerateAppPassword(t *testing.T) { defer patchNow.Unpatch() defer patchPasswordGenerate.Unpatch() + generateFromPassword := monkey.Patch(bcrypt.GenerateFromPassword, func(pw []byte, n int) ([]byte, error) { + return append([]byte("hash:"), pw...), nil + }) + defer generateFromPassword.Restore() + hashTokenXXXX, _ := bcrypt.GenerateFromPassword([]byte("XXXX"), 11) + hashToken1234, _ := bcrypt.GenerateFromPassword([]byte(token), 11) + dummyData := map[string]map[string]*apppb.AppPassword{ userpb.User{Id: &userpb.UserId{Idp: "1"}, Username: "Test User1"}.Id.String(): { - "XXXX": { - Password: "XXXX", + string(hashTokenXXXX): { + Password: string(hashTokenXXXX), Label: "", User: &userpb.UserId{Idp: "1"}, Ctime: now, @@ -162,15 +177,25 @@ func TestGenerateAppPassword(t *testing.T) { testCases := []struct { description string prevStateJSON string + expected *apppb.AppPassword expectedState map[string]map[string]*apppb.AppPassword }{ { description: "GenerateAppPassword with empty state", prevStateJSON: `{}`, + expected: &apppb.AppPassword{ + Password: token, + TokenScope: nil, + Label: "label", + User: userTest.GetId(), + Expiration: nil, + Ctime: now, + Utime: now, + }, expectedState: map[string]map[string]*apppb.AppPassword{ userTest.GetId().String(): { - token: { - Password: token, + string(hashToken1234): { + Password: string(hashToken1234), TokenScope: nil, Label: "label", User: userTest.GetId(), @@ -184,10 +209,19 @@ func TestGenerateAppPassword(t *testing.T) { { description: "GenerateAppPassword with not empty state", prevStateJSON: string(dummyDataJSON), + expected: &apppb.AppPassword{ + Password: token, + TokenScope: nil, + Label: "label", + User: userTest.GetId(), + Expiration: nil, + Ctime: now, + Utime: now, + }, expectedState: concatMaps(map[string]map[string]*apppb.AppPassword{ userTest.GetId().String(): { - token: { - Password: token, + string(hashToken1234): { + Password: string(hashToken1234), TokenScope: nil, Label: "label", User: userTest.GetId(), @@ -207,8 +241,9 @@ func TestGenerateAppPassword(t *testing.T) { defer tmpFile.Close() fill(t, tmpFile, test.prevStateJSON) manager, err := New(map[string]interface{}{ - "file": tmpFile.Name(), - "token_strength": len(token), + "file": tmpFile.Name(), + "token_strength": len(token), + "password_hash_cost": 11, }) if err != nil { t.Fatal("error creating manager:", err) @@ -221,8 +256,8 @@ func TestGenerateAppPassword(t *testing.T) { // 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(pw, test.expected) { + t.Fatalf("apppassword differ: expected=%v got=%v", render.AsCode(test.expected), render.AsCode(pw)) } if !reflect.DeepEqual(manager.(*jsonManager).passwords, test.expectedState) { @@ -267,7 +302,7 @@ func TestListAppPasswords(t *testing.T) { defer patchNow.Unpatch() now := now() - token := "1234" + token := "hash:1234" dummyDataUser0 := map[string]map[string]*apppb.AppPassword{ user0Test.GetId().String(): { @@ -394,7 +429,7 @@ func TestInvalidateAppPassword(t *testing.T) { now := now() defer patchNow.Unpatch() - token := "1234" + token := "hash:1234" dummyDataUser1Token := map[string]map[string]*apppb.AppPassword{ userTest.GetId().String(): { @@ -422,8 +457,8 @@ func TestInvalidateAppPassword(t *testing.T) { Ctime: now, Utime: now, }, - "XXXX": { - Password: "XXXX", + "hash:XXXX": { + Password: "hash:XXXX", TokenScope: nil, Label: "label", User: userTest.GetId(), @@ -465,8 +500,8 @@ func TestInvalidateAppPassword(t *testing.T) { password: token, expectedState: map[string]map[string]*apppb.AppPassword{ userTest.GetId().String(): { - "XXXX": { - Password: "XXXX", + "hash:XXXX": { + Password: "hash:XXXX", TokenScope: nil, Label: "label", User: userTest.GetId(), @@ -522,10 +557,24 @@ func TestGetAppPassword(t *testing.T) { now := now() token := "1234" + generateFromPassword := monkey.Patch(bcrypt.GenerateFromPassword, func(pw []byte, n int) ([]byte, error) { + return append([]byte("hash:"), pw...), nil + }) + compareHashAndPassword := monkey.Patch(bcrypt.CompareHashAndPassword, func(hash, pw []byte) error { + hashPw, _ := bcrypt.GenerateFromPassword(pw, 0) + if bytes.Equal(hashPw, hash) { + return nil + } + return bcrypt.ErrMismatchedHashAndPassword + }) + defer generateFromPassword.Restore() + defer compareHashAndPassword.Restore() + hashToken1234, _ := bcrypt.GenerateFromPassword([]byte(token), 11) + dummyDataUser1Token := map[string]map[string]*apppb.AppPassword{ userTest.GetId().String(): { - token: { - Password: token, + string(hashToken1234): { + Password: string(hashToken1234), TokenScope: nil, Label: "label", User: userTest.GetId(), @@ -537,8 +586,8 @@ func TestGetAppPassword(t *testing.T) { dummyDataUserExpired := map[string]map[string]*apppb.AppPassword{ userTest.GetId().String(): { - token: { - Password: token, + string(hashToken1234): { + Password: string(hashToken1234), TokenScope: nil, Label: "label", User: userTest.GetId(), @@ -552,8 +601,8 @@ func TestGetAppPassword(t *testing.T) { dummyDataUserFutureExpiration := map[string]map[string]*apppb.AppPassword{ userTest.GetId().String(): { - token: { - Password: token, + string(hashToken1234): { + Password: string(hashToken1234), TokenScope: nil, Label: "label", User: userTest.GetId(), @@ -571,8 +620,8 @@ func TestGetAppPassword(t *testing.T) { dummyDataDifferentUserToken := map[string]map[string]*apppb.AppPassword{ "OTHER_USER_ID": { - token: { - Password: token, + string(hashToken1234): { + Password: string(hashToken1234), TokenScope: nil, Label: "label", User: &userpb.UserId{Idp: "OTHER_USER_ID"}, @@ -599,14 +648,14 @@ func TestGetAppPassword(t *testing.T) { { description: "GetAppPassword with expired token", stateJSON: string(dummyDataUserExpiredJSON), - password: "TOKEN_NOT_EXISTS", + password: "1234", expectedState: nil, }, { description: "GetAppPassword with token with expiration set in the future", stateJSON: string(dummyDataUserFutureExpirationJSON), password: "1234", - expectedState: dummyDataUserFutureExpiration[userTest.GetId().String()][token], + expectedState: dummyDataUserFutureExpiration[userTest.GetId().String()][string(hashToken1234)], }, { description: "GetAppPassword with token that exists but different user", @@ -618,7 +667,7 @@ func TestGetAppPassword(t *testing.T) { description: "GetAppPassword with token that exists owned by user", stateJSON: string(dummyDataUser1TokenJSON), password: "1234", - expectedState: dummyDataUser1Token[userTest.GetId().String()][token], + expectedState: dummyDataUser1Token[userTest.GetId().String()][string(hashToken1234)], }, } diff --git a/pkg/auth/manager/loader/loader.go b/pkg/auth/manager/loader/loader.go index ad693c5b6c..adbc173848 100644 --- a/pkg/auth/manager/loader/loader.go +++ b/pkg/auth/manager/loader/loader.go @@ -20,6 +20,7 @@ package loader import ( // Load core authentication managers. + _ "github.com/cs3org/reva/pkg/auth/manager/appauth" _ "github.com/cs3org/reva/pkg/auth/manager/demo" _ "github.com/cs3org/reva/pkg/auth/manager/impersonator" _ "github.com/cs3org/reva/pkg/auth/manager/json" diff --git a/pkg/auth/scope/utils.go b/pkg/auth/scope/utils.go new file mode 100644 index 0000000000..a442db9901 --- /dev/null +++ b/pkg/auth/scope/utils.go @@ -0,0 +1,62 @@ +// 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" + link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1" + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + "github.com/cs3org/reva/pkg/errtypes" + "github.com/cs3org/reva/pkg/utils" +) + +// FormatScope create a pretty print of the scope +func FormatScope(scopeType string, scope *authpb.Scope) (string, error) { + // TODO(gmgigi96): check decoder type + switch { + case strings.HasPrefix(scopeType, "user"): + // user scope + var ref provider.Reference + err := utils.UnmarshalJSONToProtoV1(scope.Resource.Value, &ref) + if err != nil { + return "", err + } + return fmt.Sprintf("%s %s", ref.String(), scope.Role.String()), nil + case strings.HasPrefix(scopeType, "publicshare"): + // public share + var pShare link.PublicShare + err := utils.UnmarshalJSONToProtoV1(scope.Resource.Value, &pShare) + if err != nil { + return "", err + } + return fmt.Sprintf("share:\"%s\" %s", pShare.Id.OpaqueId, scope.Role.String()), nil + case strings.HasPrefix(scopeType, "resourceinfo"): + var resInfo provider.ResourceInfo + err := utils.UnmarshalJSONToProtoV1(scope.Resource.Value, &resInfo) + if err != nil { + return "", err + } + return fmt.Sprintf("path:\"%s\" %s", resInfo.Path, scope.Role.String()), nil + default: + return "", errtypes.NotSupported("scope not yet supported") + } +}