github.com/greenpau/go-authcrunch@v1.1.4/pkg/identity/database_test.go (about)

     1  // Copyright 2022 Paul Greenberg greenpau@outlook.com
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package identity
    16  
    17  import (
    18  	"fmt"
    19  	"github.com/google/go-cmp/cmp"
    20  	"github.com/greenpau/go-authcrunch/internal/tests"
    21  	"github.com/greenpau/go-authcrunch/pkg/errors"
    22  	"github.com/greenpau/go-authcrunch/pkg/requests"
    23  	"path"
    24  	"path/filepath"
    25  	"testing"
    26  	"time"
    27  )
    28  
    29  var (
    30  	testUser1     = "jsmith"
    31  	testEmail1    = "jsmith@gmail.com"
    32  	testPwd1      = tests.NewRandomString(12)
    33  	testFullName1 = "Smith, John"
    34  	testRoles1    = []string{"viewer", "editor", "admin"}
    35  	testUser2     = "bjones"
    36  	testEmail2    = "bjones@gmail.com"
    37  	testPwd2      = tests.NewRandomString(16)
    38  	testFullName2 = ""
    39  	testRoles2    = []string{"viewer"}
    40  )
    41  
    42  func createTestDatabase(s string) (*Database, error) {
    43  	tmpDir, err := tests.TempDir(s)
    44  	if err != nil {
    45  		return nil, err
    46  	}
    47  	reqs := []*requests.Request{
    48  		{
    49  			User: requests.User{
    50  				Username: testUser1,
    51  				Password: testPwd1,
    52  				Email:    testEmail1,
    53  				FullName: testFullName1,
    54  				Roles:    testRoles1,
    55  			},
    56  		},
    57  		{
    58  			User: requests.User{
    59  				Username: testUser2,
    60  				Password: testPwd2,
    61  				Email:    testEmail2,
    62  				FullName: testFullName2,
    63  				Roles:    testRoles2,
    64  			},
    65  		},
    66  	}
    67  
    68  	db, err := NewDatabase(filepath.Join(tmpDir, "user_db.json"))
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  
    73  	for _, req := range reqs {
    74  		if err := db.AddUser(req); err != nil {
    75  			return nil, err
    76  		}
    77  		user, err := db.getUser(req.User.Username)
    78  		if err != nil {
    79  			return nil, err
    80  		}
    81  		user.PublicKeys = append(user.PublicKeys, &PublicKey{})
    82  	}
    83  	return db, nil
    84  }
    85  
    86  func TestNewDatabase(t *testing.T) {
    87  	tmpDir, err := tests.TempDir("TestNewDatabase")
    88  	if err != nil {
    89  		t.Fatalf("failed to create temp dir: %v", err)
    90  	}
    91  	t.Logf("%v", tmpDir)
    92  	passwd := tests.NewRandomString(12)
    93  	testcases := []struct {
    94  		name      string
    95  		path      string
    96  		req       *requests.Request
    97  		backup    string
    98  		want      map[string]interface{}
    99  		shouldErr bool
   100  		err       error
   101  	}{
   102  		{
   103  			name: "test create new database",
   104  			path: filepath.Join(tmpDir, "user_db.json"),
   105  			req: &requests.Request{
   106  				User: requests.User{
   107  					Username: "jsmith",
   108  					Password: passwd,
   109  					Email:    "jsmith@gmail.com",
   110  					FullName: "Smith, John",
   111  					Roles:    []string{"viewer", "editor", "admin"},
   112  				},
   113  			},
   114  			backup: filepath.Join(tmpDir, "user_db_backup.json"),
   115  			want: map[string]interface{}{
   116  				"path":       filepath.Join(tmpDir, "user_db.json"),
   117  				"user_count": 0,
   118  			},
   119  		},
   120  		{
   121  			name: "test new database is directory",
   122  			path: tmpDir,
   123  			want: map[string]interface{}{
   124  				"path": tmpDir,
   125  			},
   126  			shouldErr: true,
   127  			err:       errors.ErrNewDatabase.WithArgs(tmpDir, "path points to a directory"),
   128  		},
   129  		{
   130  			name: "test load new database",
   131  			path: filepath.Join(tmpDir, "user_db.json"),
   132  			want: map[string]interface{}{
   133  				"path":       filepath.Join(tmpDir, "user_db.json"),
   134  				"user_count": 1,
   135  			},
   136  		},
   137  	}
   138  	for _, tc := range testcases {
   139  		t.Run(tc.name, func(t *testing.T) {
   140  			msgs := []string{fmt.Sprintf("test name: %s", tc.name)}
   141  			msgs = append(msgs, fmt.Sprintf("temporary directory: %s", tmpDir))
   142  			db, err := NewDatabase(tc.path)
   143  			if tests.EvalErrWithLog(t, err, "new database", tc.shouldErr, tc.err, msgs) {
   144  				return
   145  			}
   146  			got := make(map[string]interface{})
   147  			got["path"] = db.GetPath()
   148  			got["user_count"] = db.GetUserCount()
   149  			tests.EvalObjectsWithLog(t, "eval", tc.want, got, msgs)
   150  			if tc.req != nil {
   151  				if err := db.AddUser(tc.req); err != nil {
   152  					tests.EvalErrWithLog(t, err, "add user", tc.shouldErr, tc.err, msgs)
   153  				}
   154  			}
   155  			if err := db.Save(); err != nil {
   156  				t.Fatal(err)
   157  			}
   158  			if tc.backup != "" {
   159  				if err := db.Copy(tc.backup); err != nil {
   160  					t.Fatal(err)
   161  				}
   162  			}
   163  		})
   164  	}
   165  }
   166  
   167  func TestDatabaseAuthentication(t *testing.T) {
   168  	db, err := createTestDatabase("TestDatabaseAuthentication")
   169  	if err != nil {
   170  		t.Fatalf("failed to create temp dir: %v", err)
   171  	}
   172  	// t.Logf("%v", db.path)
   173  
   174  	testcases := []struct {
   175  		name      string
   176  		req       *requests.Request
   177  		want      map[string]interface{}
   178  		shouldErr bool
   179  		err       error
   180  	}{
   181  		{
   182  			name: "authenticate user1 with username",
   183  			req: &requests.Request{
   184  				User: requests.User{
   185  					Username: testUser1,
   186  					Password: testPwd1,
   187  				},
   188  				Upstream: requests.Upstream{
   189  					BaseURL:  "https://localhost",
   190  					BasePath: "/auth",
   191  					Method:   "local",
   192  					Realm:    "local",
   193  				},
   194  			},
   195  			want: map[string]interface{}{
   196  				"code":       200,
   197  				"email":      "jsmith@gmail.com",
   198  				"name":       "Smith, John",
   199  				"roles":      []string{"viewer", "editor", "admin"},
   200  				"sub":        "jsmith",
   201  				"challenges": []string{"password"},
   202  			},
   203  		},
   204  		{
   205  			name: "authenticate user2 with username",
   206  			req: &requests.Request{
   207  				User: requests.User{
   208  					Username: testUser2,
   209  					Password: testPwd2,
   210  				},
   211  				Flags: requests.Flags{
   212  					Enabled: true,
   213  				},
   214  				Upstream: requests.Upstream{
   215  					BaseURL:  "https://localhost",
   216  					BasePath: "/auth",
   217  					Method:   "local",
   218  					Realm:    "local",
   219  				},
   220  			},
   221  			want: map[string]interface{}{
   222  				"code":       200,
   223  				"email":      "bjones@gmail.com",
   224  				"roles":      []string{"viewer"},
   225  				"sub":        "bjones",
   226  				"challenges": []string{"password"},
   227  			},
   228  		},
   229  		{
   230  			name: "authenticate user1 with email address",
   231  			req: &requests.Request{
   232  				User: requests.User{
   233  					Username: testEmail1,
   234  					Password: testPwd1,
   235  				},
   236  				Upstream: requests.Upstream{
   237  					BaseURL:  "https://localhost",
   238  					BasePath: "/auth",
   239  					Method:   "local",
   240  					Realm:    "local",
   241  				},
   242  			},
   243  			want: map[string]interface{}{
   244  				"code":       200,
   245  				"email":      "jsmith@gmail.com",
   246  				"name":       "Smith, John",
   247  				"roles":      []string{"viewer", "editor", "admin"},
   248  				"sub":        "jsmith",
   249  				"challenges": []string{"password"},
   250  			},
   251  		},
   252  		{
   253  			name: "authenticate user2 with email address",
   254  			req: &requests.Request{
   255  				User: requests.User{
   256  					Username: testEmail2,
   257  					Password: testPwd2,
   258  				},
   259  				Upstream: requests.Upstream{
   260  					BaseURL:  "https://localhost",
   261  					BasePath: "/auth",
   262  					Method:   "local",
   263  					Realm:    "local",
   264  				},
   265  			},
   266  			want: map[string]interface{}{
   267  				"code":       200,
   268  				"email":      "bjones@gmail.com",
   269  				"roles":      []string{"viewer"},
   270  				"sub":        "bjones",
   271  				"challenges": []string{"password"},
   272  			},
   273  		},
   274  		{
   275  			name: "authenticate user1 with username and invalid password",
   276  			req: &requests.Request{
   277  				User: requests.User{
   278  					Username: testUser1,
   279  					Password: testPwd2,
   280  				},
   281  				Upstream: requests.Upstream{
   282  					BaseURL:  "https://localhost",
   283  					BasePath: "/auth",
   284  					Method:   "local",
   285  					Realm:    "local",
   286  				},
   287  			},
   288  			shouldErr: true,
   289  			err:       errors.ErrAuthFailed.WithArgs(errors.ErrUserPasswordInvalid),
   290  		},
   291  		{
   292  			name: "authenticate user1 with email address and invalid password",
   293  			req: &requests.Request{
   294  				User: requests.User{
   295  					Username: testEmail1,
   296  					Password: testPwd2,
   297  				},
   298  			},
   299  			shouldErr: true,
   300  			err:       errors.ErrAuthFailed.WithArgs(errors.ErrUserPasswordInvalid),
   301  		},
   302  		{
   303  			name: "authenticate with invalid username",
   304  			req: &requests.Request{
   305  				User: requests.User{
   306  					Username: "foobar",
   307  					Password: "barfoo",
   308  				},
   309  			},
   310  			shouldErr: true,
   311  			err:       errors.ErrAuthFailed.WithArgs(errors.ErrDatabaseUserNotFound),
   312  		},
   313  		{
   314  			name: "perform dummy authentication",
   315  			req: &requests.Request{
   316  				User: requests.User{
   317  					Username: "foobar",
   318  					Password: "barfoo",
   319  				},
   320  			},
   321  			shouldErr: true,
   322  			err:       errors.ErrAuthFailed.WithArgs(errors.ErrDatabaseUserNotFound),
   323  		},
   324  	}
   325  	for _, tc := range testcases {
   326  		t.Run(tc.name, func(t *testing.T) {
   327  			var err error
   328  			got := make(map[string]interface{})
   329  			msgs := []string{fmt.Sprintf("test name: %s", tc.name)}
   330  			msgs = append(msgs, fmt.Sprintf("database path: %s", db.path))
   331  			err = db.IdentifyUser(tc.req)
   332  
   333  			got["sub"] = tc.req.User.Username
   334  			got["email"] = tc.req.User.Email
   335  			if tc.req.User.FullName != "" {
   336  				got["name"] = tc.req.User.FullName
   337  			}
   338  			got["roles"] = tc.req.User.Roles
   339  			got["challenges"] = tc.req.User.Challenges
   340  
   341  			err = db.AuthenticateUser(tc.req)
   342  			if tests.EvalErrWithLog(t, err, "authenticate", tc.shouldErr, tc.err, msgs) {
   343  				return
   344  			}
   345  			got["code"] = tc.req.Response.Code
   346  			tests.EvalObjectsWithLog(t, "eval", tc.want, got, msgs)
   347  
   348  			user, err := db.getUser(tc.req.User.Username)
   349  			if err != nil {
   350  				t.Fatal(err)
   351  			}
   352  			userByID, err := db.getUserByID(user.ID)
   353  			if err != nil {
   354  				t.Fatal(err)
   355  			}
   356  			if diff := cmp.Diff(user, userByID, cmp.AllowUnexported(User{}, EmailAddress{})); diff != "" {
   357  				tests.WriteLog(t, msgs)
   358  				t.Fatalf("user by username and id mismatch (-want +got):\n%s", diff)
   359  			}
   360  
   361  			_, err = db.getUserByID("foobar")
   362  			if tests.EvalErrWithLog(t, err, "authenticate", true, errors.ErrDatabaseUserNotFound, msgs) {
   363  				return
   364  			}
   365  		})
   366  	}
   367  }
   368  
   369  func TestDatabaseAddUser(t *testing.T) {
   370  	var databasePath string
   371  	db, err := createTestDatabase("TestDatabaseAddUser")
   372  	if err != nil {
   373  		t.Fatalf("failed to create temp dir: %v", err)
   374  	}
   375  	databasePath = db.path
   376  	// t.Logf("%v", db.path)
   377  	testcases := []struct {
   378  		name          string
   379  		req           *requests.Request
   380  		overwritePath string
   381  		want          map[string]interface{}
   382  		shouldErr     bool
   383  		err           error
   384  	}{
   385  		{
   386  			name: "add user with used username",
   387  			req: &requests.Request{
   388  				User: requests.User{
   389  					Username: testUser1,
   390  					Password: testPwd1,
   391  					Email:    testEmail1,
   392  					FullName: testFullName1,
   393  					Roles:    testRoles1,
   394  				},
   395  			},
   396  			shouldErr: true,
   397  			err:       errors.ErrAddUser.WithArgs(testUser1, "username already in use"),
   398  		},
   399  		{
   400  			name: "add user with empty username",
   401  			req: &requests.Request{
   402  				User: requests.User{
   403  					Username: "",
   404  					Email:    "foobar@barfoo",
   405  				},
   406  			},
   407  			shouldErr: true,
   408  			err:       errors.ErrAddUser.WithArgs("", errors.ErrUserPolicyCompliance),
   409  		},
   410  		{
   411  			name: "add user with used email",
   412  			req: &requests.Request{
   413  				User: requests.User{
   414  					Username: "foobar",
   415  					Password: testPwd1,
   416  					Email:    testEmail1,
   417  					FullName: testFullName1,
   418  					Roles:    testRoles1,
   419  				},
   420  			},
   421  			shouldErr: true,
   422  			err:       errors.ErrAddUser.WithArgs(testEmail1, "email address already in use"),
   423  		},
   424  
   425  		{
   426  			name: "fail committing after adding new user",
   427  			req: &requests.Request{
   428  				User: requests.User{
   429  					Username: "foobar",
   430  					Password: tests.NewRandomString(16),
   431  					Email:    "foobar@barfoo",
   432  				},
   433  			},
   434  			overwritePath: path.Dir(databasePath),
   435  			shouldErr:     true,
   436  			err: errors.ErrAddUser.WithArgs("foobar",
   437  				errors.ErrDatabaseCommit.WithArgs(path.Dir(databasePath), "open "+path.Dir(databasePath)+": is a directory"),
   438  			),
   439  		},
   440  	}
   441  	for _, tc := range testcases {
   442  		t.Run(tc.name, func(t *testing.T) {
   443  			var err error
   444  			db.path = databasePath
   445  			if tc.overwritePath != "" {
   446  				db.path = tc.overwritePath
   447  			}
   448  			msgs := []string{fmt.Sprintf("test name: %s", tc.name)}
   449  			msgs = append(msgs, fmt.Sprintf("database path: %s", db.path))
   450  			err = db.AddUser(tc.req)
   451  			if tests.EvalErrWithLog(t, err, "add user", tc.shouldErr, tc.err, msgs) {
   452  				return
   453  			}
   454  			got := make(map[string]interface{})
   455  			got["user_count"] = len(db.Users)
   456  			tests.EvalObjectsWithLog(t, "user passwords", tc.want, got, msgs)
   457  		})
   458  	}
   459  }
   460  
   461  func TestDatabaseChangeUserPassword(t *testing.T) {
   462  	var databasePath string
   463  	db, err := createTestDatabase("TestDatabaseChangeUserPassword")
   464  	if err != nil {
   465  		t.Fatalf("failed to create temp dir: %v", err)
   466  	}
   467  	databasePath = db.path
   468  	// t.Logf("%v", db.path)
   469  	testcases := []struct {
   470  		name          string
   471  		req           *requests.Request
   472  		overwritePath string
   473  		want          map[string]interface{}
   474  		shouldErr     bool
   475  		err           error
   476  	}{
   477  		{
   478  			name: "change user1 password with invalid current password",
   479  			req: &requests.Request{
   480  				User: requests.User{
   481  					Username:    testUser1,
   482  					Email:       testEmail1,
   483  					OldPassword: "foobar",
   484  					Password:    tests.NewRandomString(16),
   485  				},
   486  			},
   487  			shouldErr: true,
   488  			err:       errors.ErrChangeUserPassword.WithArgs(errors.ErrUserPasswordInvalid),
   489  		},
   490  		{
   491  			name: "change user1 password",
   492  			req: &requests.Request{
   493  				User: requests.User{
   494  					Username:    testUser1,
   495  					Email:       testEmail1,
   496  					OldPassword: testPwd1,
   497  					Password:    tests.NewRandomString(16),
   498  				},
   499  			},
   500  			want: map[string]interface{}{
   501  				"password_count": 2,
   502  			},
   503  		},
   504  		{
   505  			name: "change password of invalid user",
   506  			req: &requests.Request{
   507  				User: requests.User{
   508  					Username:    "foobar",
   509  					Email:       "foobar@barfoo",
   510  					OldPassword: "foobar",
   511  					Password:    tests.NewRandomString(16),
   512  				},
   513  			},
   514  			shouldErr: true,
   515  			err:       errors.ErrChangeUserPassword.WithArgs(errors.ErrDatabaseUserNotFound),
   516  		},
   517  		{
   518  			name: "fail committing after change user2 password",
   519  			req: &requests.Request{
   520  				User: requests.User{
   521  					Username:    testUser2,
   522  					Email:       testEmail2,
   523  					OldPassword: testPwd2,
   524  					Password:    tests.NewRandomString(16),
   525  				},
   526  			},
   527  			overwritePath: path.Dir(databasePath),
   528  			shouldErr:     true,
   529  			err: errors.ErrChangeUserPassword.WithArgs(
   530  				errors.ErrDatabaseCommit.WithArgs(path.Dir(databasePath), "open "+path.Dir(databasePath)+": is a directory"),
   531  			),
   532  		},
   533  	}
   534  	for _, tc := range testcases {
   535  		t.Run(tc.name, func(t *testing.T) {
   536  			var err error
   537  			db.path = databasePath
   538  			if tc.overwritePath != "" {
   539  				db.path = tc.overwritePath
   540  			}
   541  			msgs := []string{fmt.Sprintf("test name: %s", tc.name)}
   542  			msgs = append(msgs, fmt.Sprintf("database path: %s", db.path))
   543  
   544  			err = db.ChangeUserPassword(tc.req)
   545  			if tests.EvalErrWithLog(t, err, "change password", tc.shouldErr, tc.err, msgs) {
   546  				return
   547  			}
   548  
   549  			req := &requests.Request{User: requests.User{Username: tc.req.User.Username, Password: tc.req.User.Password}}
   550  			if err := db.AuthenticateUser(req); err != nil {
   551  				t.Fatalf("expected authentication success, but got failure: %v", err)
   552  			}
   553  
   554  			user, err := db.getUser(tc.req.User.Username)
   555  			if err != nil {
   556  				t.Fatal(err)
   557  			}
   558  			got := make(map[string]interface{})
   559  			got["password_count"] = len(user.Passwords)
   560  			tests.EvalObjectsWithLog(t, "user passwords", tc.want, got, msgs)
   561  		})
   562  	}
   563  }
   564  
   565  func TestDatabaseUserPublicKey(t *testing.T) {
   566  	var databasePath string
   567  	db, err := createTestDatabase("TestDatabaseUserPublicKey")
   568  	if err != nil {
   569  		t.Fatalf("failed to create temp dir: %v", err)
   570  	}
   571  	databasePath = db.path
   572  	testcases := []struct {
   573  		name           string
   574  		operation      string
   575  		usage          string
   576  		overwriteUsage string
   577  		overwritePath  string
   578  		keyID          string
   579  		keyAlgorithm   string
   580  		pubKeyType     string
   581  		comment        string
   582  		username       string
   583  		email          string
   584  		want           map[string]interface{}
   585  		shouldErr      bool
   586  		err            error
   587  	}{
   588  		{
   589  			name:         "add disabled openssh public key",
   590  			operation:    "add",
   591  			usage:        "ssh",
   592  			keyAlgorithm: "rsa",
   593  			pubKeyType:   "openssh",
   594  			comment:      testEmail1,
   595  			username:     testUser1,
   596  			email:        testEmail1,
   597  			want: map[string]interface{}{
   598  				"key_count": 1,
   599  			},
   600  		},
   601  
   602  		{
   603  			name:         "add openssh public key",
   604  			operation:    "add",
   605  			usage:        "ssh",
   606  			keyAlgorithm: "rsa",
   607  			pubKeyType:   "openssh",
   608  			comment:      testEmail1,
   609  			username:     testUser1,
   610  			email:        testEmail1,
   611  			want: map[string]interface{}{
   612  				"key_count": 2,
   613  			},
   614  		},
   615  		{
   616  			name:         "add rsa public key",
   617  			operation:    "add",
   618  			usage:        "ssh",
   619  			keyAlgorithm: "rsa",
   620  			pubKeyType:   "openssh",
   621  			comment:      testEmail1,
   622  			username:     testUser1,
   623  			email:        testEmail1,
   624  			want: map[string]interface{}{
   625  				"key_count": 3,
   626  			},
   627  		},
   628  		{
   629  			name:      "delete all public keys",
   630  			operation: "delete",
   631  			username:  testUser1,
   632  			email:     testEmail1,
   633  			usage:     "ssh",
   634  			want: map[string]interface{}{
   635  				"key_count": 0,
   636  			},
   637  		},
   638  		{
   639  			name:         "readd rsa public key",
   640  			operation:    "add",
   641  			usage:        "ssh",
   642  			keyAlgorithm: "rsa",
   643  			pubKeyType:   "openssh",
   644  			comment:      testEmail1,
   645  			username:     testUser1,
   646  			email:        testEmail1,
   647  			want: map[string]interface{}{
   648  				"key_count": 1,
   649  			},
   650  		},
   651  		{
   652  			name:      "delete non-existing public key",
   653  			operation: "delete",
   654  			username:  testUser1,
   655  			email:     testEmail1,
   656  			usage:     "ssh",
   657  			keyID:     "foobar",
   658  			shouldErr: true,
   659  			err:       errors.ErrDeletePublicKey.WithArgs("foobar", "not found"),
   660  		},
   661  		{
   662  			name:         "add rsa public key with non-existing username",
   663  			operation:    "add",
   664  			username:     "foobar",
   665  			email:        "foobar@barfoo",
   666  			usage:        "ssh",
   667  			keyAlgorithm: "rsa",
   668  			pubKeyType:   "openssh",
   669  			shouldErr:    true,
   670  			err:          errors.ErrAddPublicKey.WithArgs("ssh", errors.ErrDatabaseUserNotFound),
   671  		},
   672  		{
   673  			name:         "add rsa public key with mismatch user and email",
   674  			operation:    "add",
   675  			username:     testUser1,
   676  			email:        testEmail2,
   677  			usage:        "ssh",
   678  			keyAlgorithm: "rsa",
   679  			pubKeyType:   "openssh",
   680  			shouldErr:    true,
   681  			err:          errors.ErrAddPublicKey.WithArgs("ssh", errors.ErrDatabaseInvalidUser),
   682  		},
   683  		{
   684  			name:         "add rsa public key with non-existing email address",
   685  			operation:    "add",
   686  			username:     testUser1,
   687  			email:        "foobar@barfoo",
   688  			usage:        "ssh",
   689  			keyAlgorithm: "rsa",
   690  			pubKeyType:   "openssh",
   691  			shouldErr:    true,
   692  			err:          errors.ErrAddPublicKey.WithArgs("ssh", errors.ErrDatabaseUserNotFound),
   693  		},
   694  		{
   695  			name:         "get public keys with non-existing email address",
   696  			operation:    "get",
   697  			username:     testUser1,
   698  			email:        "foobar@barfoo",
   699  			usage:        "ssh",
   700  			keyAlgorithm: "rsa",
   701  			pubKeyType:   "openssh",
   702  			shouldErr:    true,
   703  			err:          errors.ErrGetPublicKeys.WithArgs("ssh", errors.ErrDatabaseUserNotFound),
   704  		},
   705  
   706  		{
   707  			name:         "delete public key with non-existing email address",
   708  			operation:    "delete",
   709  			username:     testUser1,
   710  			email:        "foobar@barfoo",
   711  			usage:        "ssh",
   712  			keyAlgorithm: "rsa",
   713  			shouldErr:    true,
   714  			keyID:        "barfoo",
   715  			err:          errors.ErrDeletePublicKey.WithArgs("barfoo", errors.ErrDatabaseUserNotFound),
   716  		},
   717  		{
   718  			name:           "add invalid public key usage",
   719  			operation:      "add",
   720  			username:       testUser1,
   721  			email:          testEmail1,
   722  			usage:          "ssh",
   723  			overwriteUsage: "foobar",
   724  			keyAlgorithm:   "rsa",
   725  			pubKeyType:     "openssh",
   726  			shouldErr:      true,
   727  			err:            errors.ErrAddPublicKey.WithArgs("foobar", errors.ErrPublicKeyInvalidUsage.WithArgs("foobar")),
   728  		},
   729  		{
   730  			name:          "fail to commit when adding rsa public key",
   731  			operation:     "add",
   732  			usage:         "ssh",
   733  			keyAlgorithm:  "rsa",
   734  			pubKeyType:    "openssh",
   735  			comment:       testEmail1,
   736  			username:      testUser1,
   737  			email:         testEmail1,
   738  			overwritePath: path.Dir(databasePath),
   739  			shouldErr:     true,
   740  			err: errors.ErrAddPublicKey.WithArgs("ssh",
   741  				errors.ErrDatabaseCommit.WithArgs(path.Dir(databasePath), "open "+path.Dir(databasePath)+": is a directory"),
   742  			),
   743  		},
   744  		{
   745  			name:          "fail to commit when deleting rsa public key",
   746  			operation:     "delete",
   747  			username:      testUser1,
   748  			email:         testEmail1,
   749  			usage:         "ssh",
   750  			overwritePath: path.Dir(databasePath),
   751  			shouldErr:     true,
   752  			err: errors.ErrDeletePublicKey.WithArgs("ssh",
   753  				errors.ErrDatabaseCommit.WithArgs(path.Dir(databasePath), "open "+path.Dir(databasePath)+": is a directory"),
   754  			),
   755  		},
   756  	}
   757  	for _, tc := range testcases {
   758  		t.Run(tc.name, func(t *testing.T) {
   759  			var err error
   760  			db.path = databasePath
   761  			msgs := []string{fmt.Sprintf("test name: %s", tc.name)}
   762  			msgs = append(msgs, fmt.Sprintf("database path: %s", db.path))
   763  			switch tc.operation {
   764  			case "add":
   765  				r := requests.NewRequest()
   766  				r.User.Username = tc.username
   767  				r.User.Email = tc.email
   768  				r.Key.Usage = tc.usage
   769  				r.Key.Comment = tc.comment
   770  				_, publicKey := tests.GetCryptoKeyPair(t, tc.keyAlgorithm, tc.pubKeyType)
   771  				r.Key.Payload = publicKey
   772  				if tc.overwriteUsage != "" {
   773  					r.Key.Usage = tc.overwriteUsage
   774  				}
   775  				if tc.overwritePath != "" {
   776  					db.path = tc.overwritePath
   777  				}
   778  				err = db.AddPublicKey(r)
   779  				if tests.EvalErrWithLog(t, err, "add public key", tc.shouldErr, tc.err, msgs) {
   780  					return
   781  				}
   782  			case "get":
   783  			case "delete":
   784  				r := requests.NewRequest()
   785  				r.User.Username = tc.username
   786  				r.User.Email = tc.email
   787  				r.Key.Usage = tc.usage
   788  				err = db.GetPublicKeys(r)
   789  				if tc.keyID != "" {
   790  					// Delete specific key.
   791  					r.Key.ID = tc.keyID
   792  					err = db.DeletePublicKey(r)
   793  					if tests.EvalErrWithLog(t, err, "delete public key by id", tc.shouldErr, tc.err, msgs) {
   794  						return
   795  					}
   796  					break
   797  				}
   798  				// Delete all keys.
   799  				if tc.overwritePath != "" {
   800  					db.path = tc.overwritePath
   801  				}
   802  				bundle := r.Response.Payload.(*PublicKeyBundle)
   803  				var arr []string
   804  				for _, k := range bundle.Get() {
   805  					arr = append(arr, k.ID)
   806  				}
   807  				for _, k := range arr {
   808  					r.Key.ID = k
   809  					err = db.DeletePublicKey(r)
   810  					if tests.EvalErrWithLog(t, err, "delete public key", tc.shouldErr, tc.err, msgs) {
   811  						return
   812  					}
   813  				}
   814  			case "":
   815  				t.Fatal("empty test operation")
   816  			default:
   817  				t.Fatalf("unsupported test operation: %s", tc.operation)
   818  			}
   819  
   820  			r := requests.NewRequest()
   821  			r.User.Username = tc.username
   822  			r.User.Email = tc.email
   823  			r.Key.Usage = tc.usage
   824  			err = db.GetPublicKeys(r)
   825  			if tests.EvalErrWithLog(t, err, "get public keys", tc.shouldErr, tc.err, msgs) {
   826  				return
   827  			}
   828  			bundle := r.Response.Payload.(*PublicKeyBundle)
   829  			got := make(map[string]interface{})
   830  			got["key_count"] = bundle.Size()
   831  			tests.EvalObjectsWithLog(t, "user", tc.want, got, msgs)
   832  		})
   833  	}
   834  }
   835  
   836  func TestDatabaseUserMfaToken(t *testing.T) {
   837  	var databasePath string
   838  	db, err := createTestDatabase("TestDatabaseUserMfaToken")
   839  	if err != nil {
   840  		t.Fatalf("failed to create temp dir: %v", err)
   841  	}
   842  	databasePath = db.path
   843  	/*
   844  	   t.Logf("%v", db.path)
   845  	   for _, u := range db.Users {
   846  	           for _, n := range u.Names {
   847  	                   t.Logf("user %s, name: %v", u.Username, n)
   848  	           }
   849  	           for _, p := range u.Passwords {
   850  	                   t.Logf("user %s, password: %v", u.Username, p)
   851  	           }
   852  	   }
   853  	*/
   854  
   855  	testcases := []struct {
   856  		name          string
   857  		operation     string
   858  		req           *requests.Request
   859  		overwritePath string
   860  		want          map[string]interface{}
   861  		shouldErr     bool
   862  		err           error
   863  	}{
   864  		{
   865  			name:      "add disabled totp app token with sha1",
   866  			operation: "add",
   867  			req: &requests.Request{
   868  				User: requests.User{
   869  					Username: testUser1,
   870  					Email:    testEmail1,
   871  				},
   872  				MfaToken: requests.MfaToken{
   873  					Comment:   "ms auth app 30",
   874  					Type:      "totp",
   875  					Secret:    "c71ca4c68bc14ec5b4ab8d3c3be02ddd2c",
   876  					Algorithm: "sha1",
   877  					Period:    30,
   878  					Digits:    6,
   879  					Disabled:  true,
   880  				},
   881  			},
   882  			want: map[string]interface{}{
   883  				"token_count": 0,
   884  			},
   885  		},
   886  		{
   887  			name:      "add totp app token with sha1",
   888  			operation: "add",
   889  			req: &requests.Request{
   890  				User: requests.User{
   891  					Username: testUser1,
   892  					Email:    testEmail1,
   893  				},
   894  				MfaToken: requests.MfaToken{
   895  					Comment:   "ms auth app",
   896  					Type:      "totp",
   897  					Secret:    "c71ca4c68bc14ec5b4ab8d3c3b63802c",
   898  					Algorithm: "sha1",
   899  					Period:    30,
   900  					Digits:    6,
   901  				},
   902  			},
   903  			want: map[string]interface{}{
   904  				"token_count": 1,
   905  			},
   906  		},
   907  		{
   908  			name:      "remove all mfa tokens",
   909  			operation: "delete",
   910  			req: &requests.Request{
   911  				User: requests.User{
   912  					Username: testUser1,
   913  					Email:    testEmail1,
   914  				},
   915  				MfaToken: requests.MfaToken{},
   916  			},
   917  			want: map[string]interface{}{
   918  				"token_count": 0,
   919  			},
   920  		},
   921  		{
   922  			name:      "readd totp app token with sha1",
   923  			operation: "add",
   924  			req: &requests.Request{
   925  				User: requests.User{
   926  					Username: testUser1,
   927  					Email:    testEmail1,
   928  				},
   929  				MfaToken: requests.MfaToken{
   930  					Comment:   "ms auth app",
   931  					Type:      "totp",
   932  					Secret:    "c71ca4c68bc14ec5b4ab8d3c3b63802c",
   933  					Algorithm: "sha1",
   934  					Period:    30,
   935  					Digits:    6,
   936  				},
   937  			},
   938  			want: map[string]interface{}{
   939  				"token_count": 1,
   940  			},
   941  		},
   942  		{
   943  			name:      "delete non-existing mfa token",
   944  			operation: "delete",
   945  			req: &requests.Request{
   946  				User: requests.User{
   947  					Username: testUser1,
   948  					Email:    testEmail1,
   949  				},
   950  				MfaToken: requests.MfaToken{
   951  					ID: "foobar",
   952  				},
   953  			},
   954  			shouldErr: true,
   955  			err:       errors.ErrDeleteMfaToken.WithArgs("foobar", "not found"),
   956  		},
   957  		{
   958  			name:      "add token with non-existing username",
   959  			operation: "add",
   960  			req: &requests.Request{
   961  				User: requests.User{
   962  					Username: "foobar",
   963  					Email:    "foobar@barfoo",
   964  				},
   965  			},
   966  			shouldErr: true,
   967  			err:       errors.ErrAddMfaToken.WithArgs(errors.ErrDatabaseUserNotFound),
   968  		},
   969  		{
   970  			name:      "add token with duplicate secret",
   971  			operation: "add",
   972  			req: &requests.Request{
   973  				User: requests.User{
   974  					Username: testUser1,
   975  					Email:    testEmail1,
   976  				},
   977  				MfaToken: requests.MfaToken{
   978  					Comment:   "ms auth app",
   979  					Type:      "totp",
   980  					Secret:    "c71ca4c68bc14ec5b4ab8d3c3b63802c",
   981  					Algorithm: "sha1",
   982  					Period:    30,
   983  					Digits:    6,
   984  				},
   985  			},
   986  			shouldErr: true,
   987  			err:       errors.ErrAddMfaToken.WithArgs(errors.ErrDuplicateMfaTokenSecret),
   988  		},
   989  		{
   990  			name:      "add token with duplicate comment",
   991  			operation: "add",
   992  			req: &requests.Request{
   993  				User: requests.User{
   994  					Username: testUser1,
   995  					Email:    testEmail1,
   996  				},
   997  				MfaToken: requests.MfaToken{
   998  					Comment:   "ms auth app",
   999  					Type:      "totp",
  1000  					Secret:    "d71ca4c68bc14ec5b4ab8d3c3b63802c1",
  1001  					Algorithm: "sha1",
  1002  					Period:    30,
  1003  					Digits:    6,
  1004  				},
  1005  			},
  1006  			shouldErr: true,
  1007  			err:       errors.ErrAddMfaToken.WithArgs(errors.ErrDuplicateMfaTokenComment),
  1008  		},
  1009  
  1010  		{
  1011  			name:      "get tokens with mismatch user and email",
  1012  			operation: "get",
  1013  			req: &requests.Request{
  1014  				User: requests.User{
  1015  					Username: testUser1,
  1016  					Email:    testEmail2,
  1017  				},
  1018  			},
  1019  			shouldErr: true,
  1020  			err:       errors.ErrGetMfaTokens.WithArgs(errors.ErrDatabaseInvalidUser),
  1021  		},
  1022  
  1023  		{
  1024  			name:      "delete mfa token with mismatch user and email",
  1025  			operation: "delete",
  1026  			req: &requests.Request{
  1027  				User: requests.User{
  1028  					Username: testUser1,
  1029  					Email:    testEmail2,
  1030  				},
  1031  				MfaToken: requests.MfaToken{
  1032  					ID: "foobar",
  1033  				},
  1034  			},
  1035  			shouldErr: true,
  1036  			err:       errors.ErrDeleteMfaToken.WithArgs("foobar", errors.ErrDatabaseInvalidUser),
  1037  		},
  1038  		{
  1039  			name:      "fail to commit when adding mfa token",
  1040  			operation: "add",
  1041  			req: &requests.Request{
  1042  				User: requests.User{
  1043  					Username: testUser1,
  1044  					Email:    testEmail1,
  1045  				},
  1046  				MfaToken: requests.MfaToken{
  1047  					Comment:   "ms auth app 20",
  1048  					Type:      "totp",
  1049  					Secret:    "dd71ca4c68bc14ec5b4ab8d3c3b63802c",
  1050  					Algorithm: "sha1",
  1051  					Period:    30,
  1052  					Digits:    6,
  1053  				},
  1054  			},
  1055  			overwritePath: path.Dir(databasePath),
  1056  			shouldErr:     true,
  1057  			err: errors.ErrAddMfaToken.WithArgs(
  1058  				errors.ErrDatabaseCommit.WithArgs(path.Dir(databasePath), "open "+path.Dir(databasePath)+": is a directory"),
  1059  			),
  1060  		},
  1061  		{
  1062  			name:      "fail to commit when deleting mfa token",
  1063  			operation: "delete",
  1064  			req: &requests.Request{
  1065  				User: requests.User{
  1066  					Username: testUser1,
  1067  					Email:    testEmail1,
  1068  				},
  1069  				MfaToken: requests.MfaToken{
  1070  					ID: "zzzzzzzzzzzzzzzzzzzzzzzzzz5h3s765Tpx5Laa",
  1071  				},
  1072  			},
  1073  			overwritePath: path.Dir(databasePath),
  1074  			shouldErr:     true,
  1075  			err: errors.ErrDeleteMfaToken.WithArgs("zzzzzzzzzzzzzzzzzzzzzzzzzz5h3s765Tpx5Laa",
  1076  				errors.ErrDatabaseCommit.WithArgs(path.Dir(databasePath), "open "+path.Dir(databasePath)+": is a directory"),
  1077  			),
  1078  		},
  1079  	}
  1080  	for _, tc := range testcases {
  1081  		t.Run(tc.name, func(t *testing.T) {
  1082  			var err error
  1083  			db.path = databasePath
  1084  			msgs := []string{fmt.Sprintf("test name: %s", tc.name)}
  1085  			msgs = append(msgs, fmt.Sprintf("database path: %s", db.path))
  1086  			switch tc.operation {
  1087  			case "add":
  1088  				if tc.overwritePath != "" {
  1089  					db.path = tc.overwritePath
  1090  				}
  1091  				if tc.req.MfaToken.Type == "totp" && tc.req.MfaToken.Passcode == "" {
  1092  					if err := generateTestPasscode(tc.req, true); err != nil {
  1093  						t.Fatalf("unexpected failure during passcode generation: %v", err)
  1094  					}
  1095  				}
  1096  				err = db.AddMfaToken(tc.req)
  1097  				if tests.EvalErrWithLog(t, err, "add mfa token", tc.shouldErr, tc.err, msgs) {
  1098  					return
  1099  				}
  1100  			case "get":
  1101  			case "delete":
  1102  				if tc.overwritePath != "" {
  1103  					db.path = tc.overwritePath
  1104  				}
  1105  				err = db.GetMfaTokens(tc.req)
  1106  				if tc.req.MfaToken.ID != "" {
  1107  					// Delete specific key.
  1108  					if tc.req.MfaToken.ID == "zzzzzzzzzzzzzzzzzzzzzzzzzz5h3s765Tpx5Laa" {
  1109  						user, err := db.getUser(tc.req.User.Username)
  1110  						if err != nil {
  1111  							t.Fatal(err)
  1112  						}
  1113  						token := user.MfaTokens[0]
  1114  						token.ID = tc.req.MfaToken.ID
  1115  					}
  1116  					err = db.DeleteMfaToken(tc.req)
  1117  					if tests.EvalErrWithLog(t, err, "delete mfa token by id", tc.shouldErr, tc.err, msgs) {
  1118  						return
  1119  					}
  1120  					break
  1121  				}
  1122  				// Delete all keys.
  1123  				bundle := tc.req.Response.Payload.(*MfaTokenBundle)
  1124  				var arr []string
  1125  				for _, k := range bundle.Get() {
  1126  					arr = append(arr, k.ID)
  1127  				}
  1128  				for _, k := range arr {
  1129  					tc.req.MfaToken.ID = k
  1130  					err = db.DeleteMfaToken(tc.req)
  1131  					if tests.EvalErrWithLog(t, err, "delete mfa token", tc.shouldErr, tc.err, msgs) {
  1132  						return
  1133  					}
  1134  				}
  1135  			case "":
  1136  				t.Fatal("empty test operation")
  1137  			default:
  1138  				t.Fatalf("unsupported test operation: %s", tc.operation)
  1139  			}
  1140  
  1141  			err = db.GetMfaTokens(tc.req)
  1142  			if tests.EvalErrWithLog(t, err, "get mfa tokens", tc.shouldErr, tc.err, msgs) {
  1143  				return
  1144  			}
  1145  			bundle := tc.req.Response.Payload.(*MfaTokenBundle)
  1146  			got := make(map[string]interface{})
  1147  			got["token_count"] = bundle.Size()
  1148  			tests.EvalObjectsWithLog(t, "output", tc.want, got, msgs)
  1149  		})
  1150  	}
  1151  }
  1152  
  1153  func TestDatabaseGetUsers(t *testing.T) {
  1154  	var databasePath string
  1155  	db, err := createTestDatabase("TestDatabaseGetUsers")
  1156  	if err != nil {
  1157  		t.Fatalf("failed to create temp dir: %v", err)
  1158  	}
  1159  	ts := time.Now()
  1160  	databasePath = db.path
  1161  	testcases := []struct {
  1162  		name          string
  1163  		operation     string
  1164  		req           *requests.Request
  1165  		overwritePath string
  1166  		want          map[string]interface{}
  1167  		shouldErr     bool
  1168  		err           error
  1169  	}{
  1170  		{
  1171  			name: "get users",
  1172  			req: &requests.Request{
  1173  				User: requests.User{
  1174  					Username: testUser1,
  1175  					Email:    testEmail1,
  1176  				},
  1177  			},
  1178  			want: map[string]interface{}{
  1179  				"user_count": 2,
  1180  				"users": []*UserMetadata{
  1181  					{
  1182  						ID:           "000000000000000000000000000000000001",
  1183  						Username:     "jsmith",
  1184  						Name:         "Smith, John",
  1185  						Email:        "jsmith@gmail.com",
  1186  						LastModified: ts,
  1187  						Created:      ts,
  1188  					},
  1189  					{
  1190  						ID:           "000000000000000000000000000000000002",
  1191  						Username:     "bjones",
  1192  						Email:        "bjones@gmail.com",
  1193  						LastModified: ts,
  1194  						Created:      ts,
  1195  					},
  1196  				},
  1197  			},
  1198  		},
  1199  	}
  1200  	for _, tc := range testcases {
  1201  		t.Run(tc.name, func(t *testing.T) {
  1202  			var err error
  1203  			db.path = databasePath
  1204  			msgs := []string{fmt.Sprintf("test name: %s", tc.name)}
  1205  			msgs = append(msgs, fmt.Sprintf("database path: %s", db.path))
  1206  
  1207  			err = db.GetUsers(tc.req)
  1208  			if tests.EvalErrWithLog(t, err, "get users", tc.shouldErr, tc.err, msgs) {
  1209  				return
  1210  			}
  1211  			bundle := tc.req.Response.Payload.(*UserMetadataBundle)
  1212  			got := make(map[string]interface{})
  1213  			got["user_count"] = bundle.Size()
  1214  			users := bundle.Get()
  1215  			for i, user := range users {
  1216  				user.ID = fmt.Sprintf("%036d", i+1)
  1217  				user.LastModified = ts
  1218  				user.Created = ts
  1219  			}
  1220  			got["users"] = bundle.Get()
  1221  			tests.EvalObjectsWithLog(t, "output", tc.want, got, msgs)
  1222  		})
  1223  	}
  1224  }
  1225  
  1226  func TestDatabasePolicy(t *testing.T) {
  1227  	var databasePath string
  1228  	db, err := createTestDatabase("TestDatabasePolicy")
  1229  	if err != nil {
  1230  		t.Fatalf("failed to create temp dir: %v", err)
  1231  	}
  1232  	databasePath = db.path
  1233  	testcases := []struct {
  1234  		name          string
  1235  		operation     string
  1236  		req           *requests.Request
  1237  		overwritePath string
  1238  		want          map[string]interface{}
  1239  		shouldErr     bool
  1240  		err           error
  1241  	}{
  1242  		{
  1243  			name: "get username and password policies",
  1244  			want: map[string]interface{}{
  1245  				"username_policy": UserPolicy{
  1246  					MinLength:            3,
  1247  					MaxLength:            50,
  1248  					AllowNonAlphaNumeric: false,
  1249  					AllowUppercase:       false,
  1250  				},
  1251  				"password_policy": PasswordPolicy{
  1252  					KeepVersions:           10,
  1253  					MinLength:              8,
  1254  					MaxLength:              128,
  1255  					RequireUppercase:       false,
  1256  					RequireLowercase:       false,
  1257  					RequireNumber:          false,
  1258  					RequireNonAlphaNumeric: false,
  1259  					BlockReuse:             false,
  1260  					BlockPasswordChange:    false,
  1261  				},
  1262  				"username_policy_summary": "A username should be 3-50 character long string with lowercase, alpha-numeric characters",
  1263  				"username_policy_regex":   "^[a-z][a-z0-9]{2,49}$",
  1264  				"password_policy_summary": "A password should be 8-128 character long string",
  1265  				"password_policy_regex":   "^.{8,128}$",
  1266  			},
  1267  		},
  1268  	}
  1269  	for _, tc := range testcases {
  1270  		t.Run(tc.name, func(t *testing.T) {
  1271  			db.path = databasePath
  1272  			msgs := []string{fmt.Sprintf("test name: %s", tc.name)}
  1273  			msgs = append(msgs, fmt.Sprintf("database path: %s", db.path))
  1274  			got := make(map[string]interface{})
  1275  			got["username_policy"] = db.Policy.User
  1276  			got["password_policy"] = db.Policy.Password
  1277  			got["username_policy_regex"] = db.GetUsernamePolicyRegex()
  1278  			got["password_policy_regex"] = db.GetPasswordPolicyRegex()
  1279  			got["username_policy_summary"] = db.GetUsernamePolicySummary()
  1280  			got["password_policy_summary"] = db.GetPasswordPolicySummary()
  1281  			tests.EvalObjectsWithLog(t, "output", tc.want, got, msgs)
  1282  		})
  1283  	}
  1284  }