diff --git a/client/create.go b/client/create.go index 790a336..f59e35b 100644 --- a/client/create.go +++ b/client/create.go @@ -1,6 +1,7 @@ package client import ( + "encoding/json" "fmt" "io/ioutil" "os" @@ -13,7 +14,7 @@ func init() { } var cmdCreate = &Command{ - UsageLine: "create [--key-template template_name] ", + UsageLine: "create [--acl key_acl] [--key-template template_name] ", Short: "creates a new key", Long: ` Create will create a new key in knox with input as the primary key version. Key data should be sent to stdin unless a key-template is specified. @@ -26,7 +27,8 @@ Please run "knox create --key-template ". The original key version id will be print to stdout. -To create a new key, user credentials are required. The default access list will include the creator of this key and a limited set of site reliablity and security engineers. +Only users or SPIFFEs can create a new key. For SPIFFEs, an ACL must be provided with at least 2 users/groups set as admins. +The default ACL will include a limited set of site reliablity and security engineers, and the creator if they are a user. For more about knox, see https://github.com/pinterest/knox. @@ -34,6 +36,34 @@ See also: knox add, knox get `, } var createTinkKeyset = cmdCreate.Flag.String("key-template", "", "name of a knox-supported Tink key template") +var createAcl = cmdCreate.Flag.String("acl", "", "ACL for the created key") + +func parseAcl(aclString string) (knox.ACL, error) { + var err error + var accessList []knox.Access + + if aclString == "" { + return knox.ACL{}, nil + } + + err = json.Unmarshal([]byte(aclString), &accessList) + if err != nil { + return nil, err + } + + acl := knox.ACL(accessList) + + err = acl.Validate() + if err != nil { + return nil, err + } + err = acl.ValidateHasMultipleHumanAdmins() + if err != nil { + return nil, err + } + + return acl, nil +} func runCreate(cmd *Command, args []string) *ErrorStatus { if len(args) != 1 { @@ -55,8 +85,13 @@ func runCreate(cmd *Command, args []string) *ErrorStatus { if err != nil { return &ErrorStatus{err, false} } - // TODO(devinlundberg): allow ACL to be entered as input - acl := knox.ACL{} + + var acl knox.ACL + acl, err = parseAcl(*createAcl) + if err != nil { + return &ErrorStatus{fmt.Errorf("Error parsing ACL: %s", err.Error()), false} + } + versionID, err := cli.CreateKey(keyID, data, acl) if err != nil { return &ErrorStatus{fmt.Errorf("Error adding version: %s", err.Error()), true} diff --git a/client/create_test.go b/client/create_test.go new file mode 100644 index 0000000..0b04dd8 --- /dev/null +++ b/client/create_test.go @@ -0,0 +1,77 @@ +package client + +import ( + "testing" + + "github.com/pinterest/knox" +) + +func testAclEq(a, b knox.ACL) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func TestParseAcl(t *testing.T) { + machineAdmin := knox.Access{ID: "testmachine1", AccessType: knox.Admin, Type: knox.Machine} + userWrite := knox.Access{ID: "testuser", AccessType: knox.Write, Type: knox.User} + userAdmin := knox.Access{ID: "testuser", AccessType: knox.Admin, Type: knox.User} + groupAdmin := knox.Access{ID: "testgroup", AccessType: knox.Admin, Type: knox.UserGroup} + + validAclNoHumanAdmin := knox.ACL([]knox.Access{machineAdmin, userWrite}) + validAclWithOneHumanAdmin := knox.ACL([]knox.Access{machineAdmin, userAdmin}) + validAclWithTwoHumanAdmins := knox.ACL([]knox.Access{machineAdmin, userAdmin, groupAdmin}) + blankAcl := knox.ACL{} + + testCases := []struct { + str string + acl knox.ACL + errMsg string + }{ + { + `[{"type":"foo","id":"bar","access":"test"}]`, + validAclNoHumanAdmin, // ACL does not matter here + "json: Invalid AccessType to convert", + }, + { + `[{"type":"User","id":"testuser","access":"Write"}, {"type":"Machine","id":"testmachine1","access":"Admin"}]`, + validAclNoHumanAdmin, + "ACL needs to have at least 2 users/groups set as admins", // User only has Write access + }, + { + `[{"type":"Machine","id":"testmachine1","access":"Admin"}, {"type":"User","id":"testuser","access":"Admin"}]`, + validAclWithOneHumanAdmin, + "ACL needs to have at least 2 users/groups set as admins", // Only 1 human admin + }, + { + `[{"type":"Machine","id":"testmachine1","access":"Admin"}, {"type":"User","id":"testuser","access":"Admin"}, {"type":"UserGroup","id":"testgroup","access":"Admin"}]`, + validAclWithTwoHumanAdmins, + "", // Success, no error + }, + // Original, and default, behaviour is a blank ACL + { + ``, + blankAcl, + "", // Success, no error + }, + } + + for _, tc := range testCases { + acl, err := parseAcl(tc.str) + if tc.errMsg != "" || err != nil { + if err.Error() != tc.errMsg { + t.Fatalf("%v should equal %v", tc.errMsg, err.Error()) + } + } else{ + if !testAclEq(acl, tc.acl) { + t.Fatalf("%v should equal %v", acl, tc.acl) + } + } + } +} diff --git a/client/register_test.go b/client/register_test.go index a9e15c9..314e994 100644 --- a/client/register_test.go +++ b/client/register_test.go @@ -17,12 +17,12 @@ func TestParseTimeout(t *testing.T) { } for _, tc := range testCases { - r, err := parseTimeout(tc.str) + timeout, err := parseTimeout(tc.str) if err != nil { t.Errorf("error parsing value %s: %s", tc.str, err) continue } - if r != tc.dur { + if timeout != tc.dur { t.Errorf("mismatch: %s should parse to %s", tc.str, tc.dur.String()) } } diff --git a/client_test.go b/client_test.go index 6398a17..c677666 100644 --- a/client_test.go +++ b/client_test.go @@ -11,6 +11,10 @@ import ( "testing" ) +var DataBytes = []byte("data") +// I.e. b64encode("data") +const DataB64Encoded = "ZGF0YQ==" + func TestMockClient(t *testing.T) { p := "primary" a := []string{"active1", "active2"} @@ -181,8 +185,8 @@ func TestCreateKey(t *testing.T) { t.Fatalf("%s is not %s", r.URL.Path, "/v0/keys/") } r.ParseForm() - if r.PostForm["data"][0] != "ZGF0YQ==" { - t.Fatalf("%s is not expected: %s", r.PostForm["data"][0], "ZGF0YQ==") + if r.PostForm["data"][0] != DataB64Encoded { + t.Fatalf("%s is not expected: %s", r.PostForm["data"][0], DataB64Encoded) } if r.PostForm["id"][0] != "testkey" { t.Fatalf("%s is not expected: %s", r.PostForm["id"][0], "testkey") @@ -195,7 +199,19 @@ func TestCreateKey(t *testing.T) { cli := MockClient(srv.Listener.Addr().String(), "") - acl := ACL([]Access{ + aclWithMultipleUserAdmins := ACL([]Access{ + { + Type: User, + AccessType: Admin, + ID: "test", + }, + { + Type: User, + AccessType: Admin, + ID: "test2", + }, + }) + aclWithUserRead := ACL([]Access{ { Type: User, AccessType: Read, @@ -203,19 +219,40 @@ func TestCreateKey(t *testing.T) { }, }) - badACL := ACL([]Access{ + invalidTypeACL := ACL([]Access{ { - Type: 233, - AccessType: 80927, + Type: 233, // Not a principal + AccessType: Read, ID: "test", }, }) - _, err = cli.CreateKey("testkey", []byte("data"), badACL) - if err == nil { - t.Fatal("error is nil") + _, err = cli.CreateKey("testkey", DataBytes, invalidTypeACL) + if err.Error() != "json: error calling MarshalJSON for type knox.PrincipalType: json: Invalid PrincipalType to convert" { + t.Fatalf("Expected err is %v", err) + } + + invalidAccessTypeACL := ACL([]Access{ + { + Type: User, + AccessType: 80927, // Not an access type + ID: "test", + }, + }) + _, err = cli.CreateKey("testkey", DataBytes, invalidAccessTypeACL) + if err.Error() != "json: error calling MarshalJSON for type knox.AccessType: json: Invalid AccessType to convert" { + t.Fatalf("Expected err is %v", err) + } + + // Note: In `client/create.go` and `server/routes.go`, acl.ValidateHasMultipleHumanAdmins() would be called, but not in `client.go` + k, err := cli.CreateKey("testkey", DataBytes, aclWithUserRead) + if err != nil { + t.Fatalf("%s is not nil", err) + } + if k != expected { + t.Fatalf("%d is not %d", k, expected) } - k, err := cli.CreateKey("testkey", []byte("data"), acl) + k, err = cli.CreateKey("testkey", DataBytes, aclWithMultipleUserAdmins) if err != nil { t.Fatalf("%s is not nil", err) } @@ -238,15 +275,15 @@ func TestAddVersion(t *testing.T) { t.Fatalf("%s is not %s", r.URL.Path, "/v0/keys/testkey/versions/") } r.ParseForm() - if r.PostForm["data"][0] != "ZGF0YQ==" { - t.Fatalf("%s is not expected: %s", r.PostForm["data"][0], "ZGF0YQ==") + if r.PostForm["data"][0] != DataB64Encoded { + t.Fatalf("%s is not expected: %s", r.PostForm["data"][0], DataB64Encoded) } }) defer srv.Close() cli := MockClient(srv.Listener.Addr().String(), "") - k, err := cli.AddVersion("testkey", []byte("data")) + k, err := cli.AddVersion("testkey", DataBytes) if err != nil { t.Fatalf("%s is not nil", err) } diff --git a/knox.go b/knox.go index 1d56ddc..620265c 100644 --- a/knox.go +++ b/knox.go @@ -17,6 +17,7 @@ var ( ErrACLDuplicateEntries = fmt.Errorf("Duplicate entries in ACL") ErrACLContainsNone = fmt.Errorf("ACL contains None access") ErrACLEmptyPrincipal = fmt.Errorf("Principals of type user, user group, machine, or machine prefix may not be empty.") + ErrACLDoesNotContainMultipleHumanAdmins = fmt.Errorf("ACL needs to have at least 2 users/groups set as admins") ErrACLInvalidService = fmt.Errorf("Service is invalid, must conform to 'spiffe:///' format.") ErrACLInvalidServicePrefixURL = fmt.Errorf("Service prefix is invalid URL, must conform to 'spiffe:////' format.") @@ -43,7 +44,7 @@ const ( spiffeScheme = "spiffe" ) -// InvalidTypeError is an error for to throw when in the json conversion. +// An invalidTypeError is an error to throw when in the json conversion. type invalidTypeError struct { badType string } @@ -320,6 +321,26 @@ func (acl ACL) Validate() error { return nil } +// ValidateHasMultipleHumanAdmins ensures the ACL has at least 2 users/groups set as 'Admin's. +// This is for when SPIFFEs create keys, we want to ensure there are multiple human owners set. +// Intended to be called separately from Validate(). +func (acl ACL) ValidateHasMultipleHumanAdmins() error { + var humanAdminCount uint64 + humanAdminCount = 0 + for _, a := range acl { + if a.AccessType != Admin { + continue; + } + if a.Type == User || a.Type == UserGroup { + humanAdminCount++ + } + if humanAdminCount > 1 { + return nil + } + } + return ErrACLDoesNotContainMultipleHumanAdmins +} + // Add appends an access to the ACL. It does so by overwriting any existing access // that principal or group may have had. func (acl ACL) Add(a Access) ACL { @@ -588,6 +609,8 @@ const ( BadRequestDataCode BadKeyFormatCode BadPrincipalIdentifier + BadAclCode + NoMultipleHumanAdminsInAclCode ) // Response is the format for responses from the api server. diff --git a/knox_test.go b/knox_test.go index 8eb989b..6cf0e07 100644 --- a/knox_test.go +++ b/knox_test.go @@ -203,29 +203,71 @@ func TestKeyPathMarhaling(t *testing.T) { } func TestACLValidate(t *testing.T) { - a1 := Access{ID: "testmachine1", AccessType: Admin, Type: Machine} - a2 := Access{ID: "testuser", AccessType: Write, Type: User} - a3 := Access{ID: "testmachine", AccessType: Read, Type: MachinePrefix} - a6 := Access{ID: "spiffe://example.com/serviceA", AccessType: Read, Type: Service} - a7 := Access{ID: "spiffe://example.com/serviceA/", AccessType: Read, Type: ServicePrefix} - validACL := ACL([]Access{a1, a2, a3, a6, a7}) + var accessEntries []Access + + machineAdmin := Access{ID: "testmachine1", AccessType: Admin, Type: Machine} + userWrite := Access{ID: "testuser", AccessType: Write, Type: User} + machinePrefixRead := Access{ID: "testmachine", AccessType: Read, Type: MachinePrefix} + serviceRead := Access{ID: "spiffe://example.com/serviceA", AccessType: Read, Type: Service} + servicePrefixRead := Access{ID: "spiffe://example.com/serviceA/", AccessType: Read, Type: ServicePrefix} + + accessEntries = []Access{machineAdmin, userWrite, machinePrefixRead, serviceRead, servicePrefixRead} + validACL := ACL(accessEntries) if validACL.Validate() != nil { - t.Error("ValidACL should be valid") + t.Error("validACL should be valid") } - a4 := Access{ID: "testmachine", AccessType: None, Type: MachinePrefix} - noneACL := ACL([]Access{a1, a2, a4}) - if noneACL.Validate() == nil { - t.Error("noneACL should err") + machinePrefixNone := Access{ID: "unique", AccessType: None, Type: MachinePrefix} + accessEntriesPlusNoneACL := ACL(append(accessEntries, machinePrefixNone)) + if accessEntriesPlusNoneACL.Validate() != ErrACLContainsNone { + t.Error("accessEntriesPlusNoneACL should err") } - a5 := Access{ID: "testmachine1", AccessType: Write, Type: Machine} - dupACL := ACL([]Access{a1, a5, a3}) - if dupACL.Validate() == nil { + machineWrite := Access{ID: "testmachine1", AccessType: Write, Type: Machine} + // machineAdmin (inside accessEntries) and machineWrite have the same ID and Type + dupACL := ACL(append(accessEntries, machineWrite)) + if dupACL.Validate() != ErrACLDuplicateEntries { t.Error("dupACL should err") } } +func TestACLValidateHasMultipleHumanAdminss(t *testing.T) { + var accessEntries []Access + + machineAdmin := Access{ID: "testmachine1", AccessType: Admin, Type: Machine} + userWrite := Access{ID: "testuserwrite", AccessType: Write, Type: User} + machinePrefixRead := Access{ID: "testmachine", AccessType: Read, Type: MachinePrefix} + serviceRead := Access{ID: "spiffe://example.com/serviceA", AccessType: Read, Type: Service} + servicePrefixRead := Access{ID: "spiffe://example.com/serviceA/", AccessType: Read, Type: ServicePrefix} + + accessEntries = []Access{machineAdmin, userWrite, machinePrefixRead, serviceRead, servicePrefixRead} + // No human Admins + noHumanAdmin := ACL(accessEntries) + if noHumanAdmin.ValidateHasMultipleHumanAdmins() != ErrACLDoesNotContainMultipleHumanAdmins { + t.Error("ValidACL should not be valid") + } + + // Only 1 user Admin + userAdmin := Access{ID: "testuseradmin", AccessType: Admin, Type: User} + validWithUserAdmin := ACL(append(noHumanAdmin, userAdmin)) + if validWithUserAdmin.ValidateHasMultipleHumanAdmins() != ErrACLDoesNotContainMultipleHumanAdmins { + t.Error("ValidACL should be valid") + } + + // Only 1 group Admin + userGroupAdmin := Access{ID: "testgroup", AccessType: Admin, Type: UserGroup} + validWithGroupAdmin := ACL(append(noHumanAdmin, userGroupAdmin)) + if validWithGroupAdmin.ValidateHasMultipleHumanAdmins() != ErrACLDoesNotContainMultipleHumanAdmins { + t.Error("ValidACL should be valid") + } + + // Success, both user admin and group admin + validWithMultipleHumanAdmins := ACL(append(noHumanAdmin, userAdmin, userGroupAdmin)) + if validWithMultipleHumanAdmins.ValidateHasMultipleHumanAdmins() != nil { + t.Error("ValidACL should be valid") + } +} + func TestACLAddMultiple(t *testing.T) { a1 := Access{ID: "testmachine", AccessType: Admin, Type: Machine} a3 := Access{ID: "testmachine", AccessType: None, Type: Machine} diff --git a/server/api.go b/server/api.go index bdccfc3..9028cb2 100644 --- a/server/api.go +++ b/server/api.go @@ -12,6 +12,7 @@ import ( "github.com/pinterest/knox" "github.com/pinterest/knox/log" + "github.com/pinterest/knox/server/auth" "github.com/pinterest/knox/server/keydb" ) @@ -312,12 +313,17 @@ func newKeyVersion(d []byte, s knox.VersionStatus) knox.KeyVersion { } // NewKey creates a new Key with correctly set defaults. -func newKey(id string, acl knox.ACL, d []byte, u knox.Principal) knox.Key { +func newKey(id string, acl knox.ACL, d []byte, principal knox.Principal) knox.Key { key := knox.Key{} key.ID = id - creatorAccess := knox.Access{ID: u.GetID(), AccessType: knox.Admin, Type: knox.User} - key.ACL = acl.Add(creatorAccess) + // If principal is a service, we will have already checked `acl` for a human user or group + if auth.IsUser(principal) { + creatorAccess := knox.Access{ID: principal.GetID(), AccessType: knox.Admin, Type: knox.User} + key.ACL = acl.Add(creatorAccess) + } else { + key.ACL = acl + } for _, a := range defaultAccess { key.ACL = key.ACL.Add(a) } diff --git a/server/api_test.go b/server/api_test.go index d43c3f0..853a9b5 100644 --- a/server/api_test.go +++ b/server/api_test.go @@ -207,6 +207,7 @@ func TestNewKey(t *testing.T) { uid := "testuser" acl := knox.ACL([]knox.Access{{ID: "testmachine", AccessType: knox.Admin, Type: knox.Machine}}) data := []byte("testdata") + // User principal tests u := auth.NewUser(uid, []string{}) key := newKey(id, acl, data, u) if key.ID != id { @@ -216,12 +217,37 @@ func TestNewKey(t *testing.T) { t.Fatal("data does not match: " + string(key.VersionList[0].Data) + "!=" + string(data)) } if !u.CanAccess(key.ACL, knox.Admin) { - t.Fatal("creator does not have access to his key") + t.Fatal("User creator should have access to his key") } + + // 2 because ACL + creatorAccess if len(key.ACL) != len(defaultAccess)+2 { text, _ := json.Marshal(key.ACL) - t.Fatal("The Key's ACL is too big: " + string(text)) + t.Fatal("The Key's ACL has unexpected length: " + string(text)) + } + + // Service principal tests + s := auth.NewService("example.com", "serviceA") + key = newKey(id, acl, data, s) + if key.ID != id { + t.Fatal("ID does not match: " + key.ID + "!=" + id) } + if len(key.VersionList) != 1 || !bytes.Equal(key.VersionList[0].Data, data) { + t.Fatal("data does not match: " + string(key.VersionList[0].Data) + "!=" + string(data)) + } + if s.CanAccess(key.ACL, knox.Admin) { + t.Fatal("Service creator should not have access to his key") + } + + // 1 because only ACL and no creatorAccess + if len(key.ACL) != len(defaultAccess)+1 { + text, err := json.Marshal(key.ACL) + if err != nil { + t.Fatal("Error is: " + err.Error()) + } + t.Fatal("The Key's ACL has unexpected length: " + string(text)) + } + } diff --git a/server/routes.go b/server/routes.go index 032d5b5..25c6635 100644 --- a/server/routes.go +++ b/server/routes.go @@ -138,10 +138,29 @@ func getKeysHandler(m KeyManager, principal knox.Principal, parameters map[strin // The route for this handler is POST /v0/keys/ // The postKeysHandler must be a User. func postKeysHandler(m KeyManager, principal knox.Principal, parameters map[string]string) (interface{}, *HTTPError) { + var err error + aclStr, aclOK := parameters["acl"] - // Authorize - if !auth.IsUser(principal) { - return nil, errF(knox.UnauthorizedCode, fmt.Sprintf("Must be a user to create keys, principal is %s", principal.GetID())) + acl := make(knox.ACL, 0) + if aclOK { + jsonErr := json.Unmarshal([]byte(aclStr), &acl) + if jsonErr != nil { + return nil, errF(knox.BadAclCode, jsonErr.Error()) + } + } + + // Only users -- and SPIFFEs when at latest 2 human users/groups are admins in the ACL -- can create keys + if auth.IsService(principal) { + err = acl.Validate() + if err != nil { + return nil, errF(knox.BadAclCode, "Error validating parameter 'acl'") + } + err = acl.ValidateHasMultipleHumanAdmins() + if err != nil { + return nil, errF(knox.NoMultipleHumanAdminsInAclCode, "Parameter 'acl' does not have multiple human admins") + } + } else if !auth.IsUser(principal) { + return nil, errF(knox.UnauthorizedCode, fmt.Sprintf("Must be a user (or SPIFFE if multiple human admins in ACL) to create keys, principal is %s", principal.GetID())) } keyID, keyIDOK := parameters["id"] @@ -155,15 +174,6 @@ func postKeysHandler(m KeyManager, principal knox.Principal, parameters map[stri if data == "" { return nil, errF(knox.NoKeyDataCode, "Parameter 'data' is empty") } - aclStr, aclOK := parameters["acl"] - - acl := make(knox.ACL, 0) - if aclOK { - jsonErr := json.Unmarshal([]byte(aclStr), &acl) - if jsonErr != nil { - return nil, errF(knox.BadRequestDataCode, jsonErr.Error()) - } - } decodedData, decodeErr := base64.StdEncoding.DecodeString(data) if decodeErr != nil { @@ -172,7 +182,7 @@ func postKeysHandler(m KeyManager, principal knox.Principal, parameters map[stri // Create and add new key key := newKey(keyID, acl, decodedData, principal) - err := m.AddNewKey(&key) + err = m.AddNewKey(&key) if err != nil { if err == knox.ErrKeyExists { return nil, errF(knox.KeyIdentifierExistsCode, fmt.Sprintf("Key %s already exists", keyID)) diff --git a/server/routes_test.go b/server/routes_test.go index b70f209..7dc0747 100644 --- a/server/routes_test.go +++ b/server/routes_test.go @@ -10,6 +10,10 @@ import ( "github.com/pinterest/knox/server/keydb" ) +const Number1 = "1" +// I.e. b64encode("1") +const Number1B64Encoded = "MQ==" + func makeDB() (KeyManager, *keydb.TempDB) { db := &keydb.TempDB{} cryptor := keydb.NewAESGCMCryptor(0, []byte("testtesttesttest")) @@ -21,7 +25,7 @@ func TestGetKeys(t *testing.T) { m, db := makeDB() u := auth.NewUser("testuser", []string{}) - _, err := postKeysHandler(m, u, map[string]string{"id": "a1", "data": "MQ=="}) + _, err := postKeysHandler(m, u, map[string]string{"id": "a1", "data": Number1B64Encoded}) if err != nil { t.Fatalf("%+v is not nil", err) } @@ -86,64 +90,154 @@ func TestGetKeys(t *testing.T) { func TestPostKeys(t *testing.T) { m, db := makeDB() + + // Machine tests machine := auth.NewMachine("MrRoboto") - _, err := postKeysHandler(m, machine, map[string]string{"id": "a1", "data": "MQ=="}) + // Machines cannot create keys + _, err := postKeysHandler(m, machine, map[string]string{"id": "a1", "data": Number1B64Encoded}) if err == nil { t.Fatal("Expected err") + } else if err.Subcode != knox.UnauthorizedCode { + t.Fatalf("Expected %v and got %v", knox.UnauthorizedCode, err.Subcode) + } else if err.Message != "Must be a user (or SPIFFE if multiple human admins in ACL) to create keys, principal is MrRoboto" { + t.Fatalf("Unexpected error message: %v", err.Message) } - u := auth.NewUser("testuser", []string{}) + // Service tests + serviceA := auth.NewService("example.com", "serviceA") + // ACL JSON but still Invalid + _, err = postKeysHandler(m, serviceA, map[string]string{"id": "a1", "data": Number1B64Encoded, "acl": `[{"type":"foo","id":"bar","access":"test"}]`}) + if err == nil { + t.Fatal("Expected err") + } else if err.Subcode != knox.BadAclCode { + t.Fatalf("Expected %v and got %v", knox.BadAclCode, err.Subcode) + } else if err.Message != "json: Invalid AccessType to convert" { + t.Fatalf("Unexpected error message: %v", err.Message) + } + // Valid ACL but no human admin + _, err = postKeysHandler(m, serviceA, map[string]string{"id": "a1", "data": Number1B64Encoded, "acl": `[{"type":"User","id":"testuser","access":"Write"}, {"type":"Machine","id":"testmachine1","access":"Admin"}]`}) + if err == nil { + t.Fatal("Expected err") + } else if err.Subcode != knox.NoMultipleHumanAdminsInAclCode { + t.Fatalf("Expected %v and got %v", knox.NoMultipleHumanAdminsInAclCode, err.Subcode) + } else if err.Message != "Parameter 'acl' does not have multiple human admins" { + t.Fatalf("Unexpected error message: %v", err.Message) + } + // Valid ACL with only 1 human admin + _, err = postKeysHandler(m, serviceA, map[string]string{"id": "a0", "data": Number1B64Encoded, "acl": `[{"type":"User","id":"testuser","access":"Admin"}, {"type":"Machine","id":"testmachine1","access":"Admin"}]`}) + if err == nil { + t.Fatal("Expected err") + } else if err.Subcode != knox.NoMultipleHumanAdminsInAclCode { + t.Fatalf("Expected %v and got %v", knox.NoMultipleHumanAdminsInAclCode, err.Subcode) + } else if err.Message != "Parameter 'acl' does not have multiple human admins" { + t.Fatalf("Unexpected error message: %v", err.Message) + } + // Valid ACL with 2 human admins + _, err = postKeysHandler(m, serviceA, map[string]string{"id": "a0", "data": Number1B64Encoded, "acl": `[{"type":"User","id":"testuser","access":"Admin"}, {"type":"User","id":"testuser2","access":"Admin"}]`}) + if err != nil { + t.Fatalf("%+v is not nil", err) + } + + // User tests + testuser := auth.NewUser("testuser", []string{}) - _, err = postKeysHandler(m, u, map[string]string{"data": "MQ=="}) + // No id + _, err = postKeysHandler(m, testuser, map[string]string{"data": Number1B64Encoded}) if err == nil { t.Fatal("Expected err") + } else if err.Subcode != knox.NoKeyIDCode { + t.Fatalf("Expected %v and got %v", knox.NoKeyIDCode, err.Subcode) + } else if err.Message != "Missing parameter 'id'" { + t.Fatalf("Unexpected error message: %v", err.Message) } - _, err = postKeysHandler(m, u, map[string]string{"id": "a1"}) + // No data + _, err = postKeysHandler(m, testuser, map[string]string{"id": "a1"}) if err == nil { t.Fatal("Expected err") + } else if err.Subcode != knox.NoKeyDataCode { + t.Fatalf("Expected %v and got %v", knox.NoKeyDataCode, err.Subcode) + } else if err.Message != "Missing parameter 'data'" { + t.Fatalf("Unexpected error message: %v", err.Message) } - _, err = postKeysHandler(m, u, map[string]string{"id": "a1", "data": "MQ==", "acl": "NOTJSON"}) + // ACL not JSON + _, err = postKeysHandler(m, testuser, map[string]string{"id": "a1", "data": Number1B64Encoded, "acl": "NOTJSON"}) if err == nil { t.Fatal("Expected err") + } else if err.Subcode != knox.BadAclCode { + t.Fatalf("Expected %v and got %v", knox.BadAclCode, err.Subcode) + } else if err.Message != "invalid character 'N' looking for beginning of value" { + t.Fatalf("Unexpected error message: %v", err.Message) } - _, err = postKeysHandler(m, u, map[string]string{"id": "a1", "data": "NotBAse64.."}) + // ACL JSON but still Invalid + _, err = postKeysHandler(m, testuser, map[string]string{"id": "a1", "data": Number1B64Encoded, "acl": `[{"type":"foo","id":"bar","access":"test"}]`}) if err == nil { t.Fatal("Expected err") + } else if err.Subcode != knox.BadAclCode { + t.Fatalf("Expected %v and got %v", knox.BadAclCode, err.Subcode) + } else if err.Message != "json: Invalid AccessType to convert" { + t.Fatalf("Unexpected error message: %v", err.Message) } - _, err = postKeysHandler(m, u, map[string]string{"id": "a$#", "data": "MQ=="}) + // Base64 decode error on Data + _, err = postKeysHandler(m, testuser, map[string]string{"id": "a1", "data": "NotBAse64.."}) if err == nil { t.Fatal("Expected err") + } else if err.Subcode != knox.BadRequestDataCode { + t.Fatalf("Expected %v and got %v", knox.BadRequestDataCode, err.Subcode) + } else if err.Message != "illegal base64 data at input byte 9" { + t.Fatalf("Unexpected error message: %v", err.Message) } - i, err := postKeysHandler(m, u, map[string]string{"id": "a1", "data": "MQ=="}) + // Invalid KeyID + _, err = postKeysHandler(m, testuser, map[string]string{"id": "a$#", "data": Number1B64Encoded}) + if err == nil { + t.Fatal("Expected err") + } else if err.Subcode != knox.BadKeyFormatCode { + t.Fatalf("Expected %v and got %v", knox.BadKeyFormatCode, err.Subcode) + } else if err.Message != "KeyID includes unsupported characters a$#" { + t.Fatalf("Unexpected error message: %v", err.Message) + } + + // Make a1 key + a1KeyID, err := postKeysHandler(m, testuser, map[string]string{"id": "a1", "data": Number1B64Encoded}) if err != nil { t.Fatalf("%+v is not nil", err) } - _, err = postKeysHandler(m, u, map[string]string{"id": "a1", "data": "MQ=="}) + // Key already exists + _, err = postKeysHandler(m, testuser, map[string]string{"id": "a1", "data": Number1B64Encoded}) if err == nil { t.Fatal("Expected err") + } else if err.Subcode != knox.KeyIdentifierExistsCode { + t.Fatalf("Expected %v and got %v", knox.KeyIdentifierExistsCode, err.Subcode) + } else if err.Message != "Key a1 already exists" { + t.Fatalf("Unexpected error message: %v", err.Message) } - _, err = postKeysHandler(m, u, map[string]string{"id": "a1", "data": ""}) + // Empty data + _, err = postKeysHandler(m, testuser, map[string]string{"id": "a1", "data": ""}) if err == nil { t.Fatal("Expected err") + } else if err.Subcode != knox.NoKeyDataCode { + t.Fatalf("Expected %v and got %v", knox.NoKeyDataCode, err.Subcode) + } else if err.Message != "Parameter 'data' is empty" { + t.Fatalf("Unexpected error message: %v", err.Message) } - j, err := postKeysHandler(m, u, map[string]string{"id": "a2", "data": "MQ==", "acl": "[]"}) + // Make a2 key + a2KeyID, err := postKeysHandler(m, testuser, map[string]string{"id": "a2", "data": Number1B64Encoded, "acl": "[]"}) if err != nil { t.Fatalf("%+v is not nil", err) } - switch q := i.(type) { + switch q := a1KeyID.(type) { default: t.Fatal("Unexpected type of response") case uint64: - switch r := j.(type) { + switch r := a2KeyID.(type) { default: t.Fatal("Unexpected type of response") case uint64: @@ -154,9 +248,13 @@ func TestPostKeys(t *testing.T) { } db.SetError(fmt.Errorf("Test Error")) - _, err = postKeysHandler(m, u, map[string]string{"id": "a3", "data": "MQ=="}) + _, err = postKeysHandler(m, testuser, map[string]string{"id": "a3", "data": Number1B64Encoded}) if err == nil { t.Fatal("Expected err") + } else if err.Subcode != knox.InternalServerErrorCode { + t.Fatalf("Expected %v and got %v", knox.InternalServerErrorCode, err.Subcode) + } else if err.Message != "Test Error" { + t.Fatalf("Unexpected error message: %v", err.Message) } } @@ -165,7 +263,7 @@ func TestGetKey(t *testing.T) { machine := auth.NewMachine("MrRoboto") u := auth.NewUser("testuser", []string{}) - _, err := postKeysHandler(m, u, map[string]string{"id": "a1", "data": "MQ=="}) + _, err := postKeysHandler(m, u, map[string]string{"id": "a1", "data": Number1B64Encoded}) if err != nil { t.Fatalf("%+v is not nil", err) } @@ -184,7 +282,7 @@ func TestGetKey(t *testing.T) { if len(k.VersionList) != 1 { t.Fatalf("Expected len to be 1 not %d", len(k.VersionList)) } - if string(k.VersionList[0].Data) != "1" { + if string(k.VersionList[0].Data) != Number1 { t.Fatalf("Expected ID to be a1 not %s", string(k.VersionList[0].Data)) } } @@ -203,7 +301,7 @@ func TestGetKey(t *testing.T) { if len(k.VersionList) != 1 { t.Fatalf("Expected len to be 1 not %d", len(k.VersionList)) } - if string(k.VersionList[0].Data) != "1" { + if string(k.VersionList[0].Data) != Number1 { t.Fatalf("Expected ID to be a1 not %s", string(k.VersionList[0].Data)) } } @@ -222,7 +320,7 @@ func TestGetKey(t *testing.T) { if len(k.VersionList) != 1 { t.Fatalf("Expected len to be 1 not %d", len(k.VersionList)) } - if string(k.VersionList[0].Data) != "1" { + if string(k.VersionList[0].Data) != Number1 { t.Fatalf("Expected ID to be a1 not %s", string(k.VersionList[0].Data)) } } @@ -247,7 +345,7 @@ func TestDeleteKey(t *testing.T) { m, db := makeDB() u := auth.NewUser("testuser", []string{}) machine := auth.NewMachine("MrRoboto") - _, err := postKeysHandler(m, u, map[string]string{"id": "a1", "data": "MQ=="}) + _, err := postKeysHandler(m, u, map[string]string{"id": "a1", "data": Number1B64Encoded}) if err != nil { t.Fatalf("%+v is not nil", err) } @@ -289,7 +387,7 @@ func TestGetAccess(t *testing.T) { m, _ := makeDB() u := auth.NewUser("testuser", []string{}) machine := auth.NewMachine("MrRoboto") - _, err := postKeysHandler(m, u, map[string]string{"id": "a1", "data": "MQ=="}) + _, err := postKeysHandler(m, u, map[string]string{"id": "a1", "data": Number1B64Encoded}) if err != nil { t.Fatalf("%+v is not nil", err) } @@ -333,7 +431,7 @@ func TestPutAccess(t *testing.T) { u := auth.NewUser("testuser", []string{}) machine := auth.NewMachine("MrRoboto") - _, err := postKeysHandler(m, u, map[string]string{"id": "a1", "data": "MQ=="}) + _, err := postKeysHandler(m, u, map[string]string{"id": "a1", "data": Number1B64Encoded}) if err != nil { t.Fatalf("%+v is not nil", err) } @@ -399,7 +497,7 @@ func TestLegacyPutAccess(t *testing.T) { u := auth.NewUser("testuser", []string{}) machine := auth.NewMachine("MrRoboto") - _, err := postKeysHandler(m, u, map[string]string{"id": "a1", "data": "MQ=="}) + _, err := postKeysHandler(m, u, map[string]string{"id": "a1", "data": Number1B64Encoded}) if err != nil { t.Fatalf("%+v is not nil", err) } @@ -463,7 +561,7 @@ func TestPostVersion(t *testing.T) { m, db := makeDB() u := auth.NewUser("testuser", []string{}) machine := auth.NewMachine("MrRoboto") - j, err := postKeysHandler(m, u, map[string]string{"id": "a1", "data": "MQ=="}) + j, err := postKeysHandler(m, u, map[string]string{"id": "a1", "data": Number1B64Encoded}) if err != nil { t.Fatalf("%+v is not nil", err) } @@ -525,7 +623,7 @@ func TestPutVersions(t *testing.T) { m, db := makeDB() u := auth.NewUser("testuser", []string{}) machine := auth.NewMachine("MrRoboto") - i, err := postKeysHandler(m, u, map[string]string{"id": "a1", "data": "MQ=="}) + i, err := postKeysHandler(m, u, map[string]string{"id": "a1", "data": Number1B64Encoded}) if err != nil { t.Fatalf("%+v is not nil", err) }