diff --git a/go.mod b/go.mod index 2321cb02137..535a3bb77ca 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 @@ -17,6 +18,7 @@ require ( github.com/cs3org/go-cs3apis v0.0.0-20210507060801-f176760d55f4 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-openapi/errors v0.19.6 // indirect github.com/go-openapi/strfmt v0.19.2 // indirect @@ -43,6 +45,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 610f603ad29..9e01f019b27 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= @@ -184,6 +186,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/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8= github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= @@ -965,6 +969,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/pkg/appauth/manager/json/json.go b/pkg/appauth/manager/json/json.go index 1f0b768a044..af4f3afad36 100644 --- a/pkg/appauth/manager/json/json.go +++ b/pkg/appauth/manager/json/json.go @@ -17,3 +17,213 @@ // 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.BadRequest("password not found") + } + + if _, ok := appPasswords[password]; !ok { + return errtypes.BadRequest("password not found") + } + delete(appPasswords, 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.BadRequest("password not found") + } + + pw, ok := appPassword[password] + if !ok { + return nil, errtypes.BadRequest("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 index 1f0b768a044..524150b0181 100644 --- a/pkg/appauth/manager/json/json_test.go +++ b/pkg/appauth/manager/json/json_test.go @@ -17,3 +17,602 @@ // 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, + }, + }} + + dummyDataUser0JSON, _ := json.Marshal(dummyDataUser0) + + 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 (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, + }, + }} + + dummyDataUser1TokenJSON, _ := json.Marshal(dummyDataUser1Token) + + 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 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 +}