diff --git a/api/client.go b/api/client.go index 2696a76b4..4b37bd95c 100644 --- a/api/client.go +++ b/api/client.go @@ -196,38 +196,46 @@ type GenCRLResponse struct { // AddIdentityRequest represents the request to add a new identity to the // fabric-ca-server type AddIdentityRequest struct { - IdentityInfo `json:"info"` + ID string `json:"id" skip:"true"` + Type string `json:"type" def:"user" help:"Type of identity being registered (e.g. 'peer, app, user')"` + Affiliation string `json:"affiliation" help:"The identity's affiliation"` + Attributes []Attribute `json:"attrs" mapstructure:"attrs" ` + MaxEnrollments int `json:"max_enrollments" mapstructure:"max_enrollments" def:"-1" help:"The maximum number of times the secret can be reused to enroll."` // Secret is an optional password. If not specified, // a random secret is generated. In both cases, the secret // is returned in the RegistrationResponse. - Secret string `json:"secret,omitempty" mask:"password" help:"The enrollment secret for the identity being registered"` + Secret string `json:"secret,omitempty" mask:"password" help:"The enrollment secret for the identity being added"` CAName string `json:"caname,omitempty" skip:"true"` } // ModifyIdentityRequest represents the request to modify an existing identity on the // fabric-ca-server type ModifyIdentityRequest struct { - ID string `json:"id"` - IdentityInfo `json:"info"` - // Secret is an optional password. If not specified, - // a random secret is generated. In both cases, the secret - // is returned in the RegistrationResponse.a - Secret string `json:"secret,omitempty" mask:"password"` - CAName string `json:"caname,omitempty" skip:"true"` + ID string `skip:"true"` + Type string `json:"type" def:"user" help:"Type of identity being registered (e.g. 'peer, app, user')"` + Affiliation string `json:"affiliation" help:"The identity's affiliation"` + Attributes []Attribute `mapstructure:"attrs" json:"attrs"` + MaxEnrollments int `mapstructure:"max_enrollments" json:"max_enrollments" def:"-1" help:"The maximum number of times the secret can be reused to enroll."` + Secret string `json:"secret,omitempty" mask:"password" help:"The enrollment secret for the identity"` + CAName string `json:"caname,omitempty" skip:"true"` } // RemoveIdentityRequest represents the request to remove an existing identity from the // fabric-ca-server type RemoveIdentityRequest struct { - ID string `json:"id" skip:"true"` + ID string `skip:"true"` Force bool `json:"force"` CAName string `json:"caname,omitempty" skip:"true"` } // GetIDResponse is the response from the GetIdentity call type GetIDResponse struct { - IdentityInfo `mapstructure:",squash"` - CAName string `json:"caname,omitempty"` + ID string `json:"id" skip:"true"` + Type string `json:"type" def:"user"` + Affiliation string `json:"affiliation"` + Attributes []Attribute `json:"attrs" mapstructure:"attrs" ` + MaxEnrollments int `json:"max_enrollments" mapstructure:"max_enrollments"` + CAName string `json:"caname,omitempty"` } // GetAllIDsResponse is the response from the GetAllIdentities call @@ -238,67 +246,61 @@ type GetAllIDsResponse struct { // IdentityResponse is the response from the any add/modify/remove identity call type IdentityResponse struct { - IdentityInfo `mapstructure:",squash"` - Secret string `json:"secret,omitempty"` - CAName string `json:"caname,omitempty"` + ID string `json:"id" skip:"true"` + Type string `json:"type,omitempty"` + Affiliation string `json:"affiliation"` + Attributes []Attribute `json:"attrs,omitempty" mapstructure:"attrs"` + MaxEnrollments int `json:"max_enrollments,omitempty" mapstructure:"max_enrollments"` + Secret string `json:"secret,omitempty"` + CAName string `json:"caname,omitempty"` } // IdentityInfo contains information about an identity type IdentityInfo struct { - ID string `json:"id" skip:"true"` - Type string `json:"type" def:"user" help:"Type of identity being registered (e.g. 'peer, app, user')"` - Affiliation string `json:"affiliation" help:"The identity's affiliation"` - Attributes []Attribute `mapstructure:"attrs" json:"attrs"` - MaxEnrollments int `mapstructure:"max_enrollments" json:"max_enrollments" def:"-1" help:"The maximum number of times the secret can be reused to enroll."` -} - -// GetAllAffiliationsResponse is the response from the GetAllAffiliations call -type GetAllAffiliationsResponse struct { - Affiliations []AffiliationInfo `json:"affiliations"` - CAName string `json:"caname,omitempty"` + ID string `json:"id"` + Type string `json:"type"` + Affiliation string `json:"affiliation"` + Attributes []Attribute `json:"attrs" mapstructure:"attrs"` + MaxEnrollments int `json:"max_enrollments" mapstructure:"max_enrollments"` } // AddAffiliationRequest represents the request to add a new affiliation to the // fabric-ca-server type AddAffiliationRequest struct { - Info AffiliationInfo `json:"info"` - Force bool `json:"force"` - CAName string `json:"caname,omitempty"` + Name string `json:"name"` + Force bool `json:"force"` + CAName string `json:"caname,omitempty"` } // ModifyAffiliationRequest represents the request to modify an existing affiliation on the // fabric-ca-server type ModifyAffiliationRequest struct { - Name string `json:"name"` - Info AffiliationInfo `json:"info"` - Force bool `json:"force"` - CAName string `json:"caname,omitempty"` + Name string + NewName string `json:"name"` + Force bool `json:"force"` + CAName string `json:"caname,omitempty"` } // RemoveAffiliationRequest represents the request to remove an existing affiliation from the // fabric-ca-server type RemoveAffiliationRequest struct { - Name string `json:"name"` + Name string Force bool `json:"force"` CAName string `json:"caname,omitempty"` } -// AffiliationWithIdentityResponse contains the response from removing an affiliation request -type AffiliationWithIdentityResponse struct { - Affiliations []AffiliationInfo `json:"affiliations"` - Identities []IdentityInfo `json:"identities"` - CAName string `json:"caname,omitempty"` -} - -// AffiliationResponse is the response from the any add/modify/remove affiliation call +// AffiliationResponse contains the response for get, add, modify, and remove an affiliation type AffiliationResponse struct { - Info AffiliationInfo `json:"info"` - CAName string `json:"caname,omitempty"` + AffiliationInfo `mapstructure:",squash"` + CAName string `json:"caname,omitempty"` } -// AffiliationInfo contains information about the affiliation +// AffiliationInfo contains the affiliation name, child affiliation info, and identities +// associated with this affiliation. type AffiliationInfo struct { - Name string `json:"name"` + Name string `json:"name"` + Affiliations []AffiliationInfo `json:"affiliations,omitempty"` + Identities []IdentityInfo `json:"identities,omitempty"` } // CSRInfo is Certificate Signing Request (CSR) Information diff --git a/api/net.go b/api/net.go index 3c4672afa..15677d34c 100644 --- a/api/net.go +++ b/api/net.go @@ -87,12 +87,12 @@ type ModifyIdentityRequestNet struct { // AddAffiliationRequestNet is a network request for adding a new affiliation type AddAffiliationRequestNet struct { - Info AffiliationInfo `json:"info"` + AddAffiliationRequest } // ModifyAffiliationRequestNet is a network request for modifying an existing affiliation type ModifyAffiliationRequestNet struct { - Info AffiliationInfo `json:"info"` + ModifyAffiliationRequest } // KeySig is a public key, signature, and signature algorithm tuple diff --git a/cmd/fabric-ca-client/affiliation.go b/cmd/fabric-ca-client/affiliation.go index b93af22ed..926fa8c98 100644 --- a/cmd/fabric-ca-client/affiliation.go +++ b/cmd/fabric-ca-client/affiliation.go @@ -21,7 +21,6 @@ import ( "github.com/cloudflare/cfssl/log" "github.com/hyperledger/fabric-ca/api" - "github.com/hyperledger/fabric-ca/lib" "github.com/spf13/cobra" ) @@ -93,7 +92,7 @@ func (c *ClientCmd) newModifyAffiliationCommand() *cobra.Command { } flags := affiliationModifyCmd.Flags() flags.StringVarP( - &c.dynamicAffiliation.modify.Info.Name, "name", "", "", "Rename the affiliation") + &c.dynamicAffiliation.modify.NewName, "name", "", "", "Rename the affiliation") flags.BoolVarP( &c.dynamicAffiliation.modify.Force, "force", "", false, "Forces identities using old affiliation to use new affiliation") return affiliationModifyCmd @@ -128,15 +127,16 @@ func (c *ClientCmd) runListAffiliation(cmd *cobra.Command, args []string) error return err } - fmt.Printf("%+v\n", resp.Info) + printTree(resp) return nil } - err = id.GetAllAffiliations(c.clientCfg.CAName, lib.AffiliationDecoder) + resp, err := id.GetAllAffiliations(c.clientCfg.CAName) if err != nil { return err } + printTree(resp) return nil } @@ -150,7 +150,7 @@ func (c *ClientCmd) runAddAffiliation(cmd *cobra.Command, args []string) error { } req := &api.AddAffiliationRequest{} - req.Info.Name = args[0] + req.Name = args[0] req.CAName = c.clientCfg.CAName req.Force = c.dynamicAffiliation.add.Force @@ -159,7 +159,7 @@ func (c *ClientCmd) runAddAffiliation(cmd *cobra.Command, args []string) error { return err } - fmt.Printf("Successfully added affiliation: %+v\n", resp) + fmt.Printf("Successfully added affiliation: %+v\n", resp.Name) return nil } @@ -175,7 +175,7 @@ func (c *ClientCmd) runModifyAffiliation(cmd *cobra.Command, args []string) erro req := &api.ModifyAffiliationRequest{} req.Name = args[0] - req.Info.Name = c.dynamicAffiliation.modify.Info.Name + req.NewName = c.dynamicAffiliation.modify.NewName req.CAName = c.clientCfg.CAName req.Force = c.dynamicAffiliation.modify.Force @@ -228,3 +228,26 @@ func (c *ClientCmd) affiliationPreRunE(cmd *cobra.Command, args []string) error return nil } + +func printTree(resp *api.AffiliationResponse) { + root := resp.Name + if root == "" { + root = "." + } + fmt.Printf("affiliation: %s\n", root) + printChildren(resp.Affiliations, 1) +} + +func printChildren(children []api.AffiliationInfo, level int) { + if len(children) == 0 { + return + } + for _, child := range children { + spaces := "" + for i := 0; i < level; i++ { + spaces = spaces + " " + } + fmt.Printf("%saffiliation :%s\n", spaces, child.Name) + printChildren(child.Affiliations, level+1) + } +} diff --git a/cmd/fabric-ca-client/identity.go b/cmd/fabric-ca-client/identity.go index cc8435dab..1f6686bec 100644 --- a/cmd/fabric-ca-client/identity.go +++ b/cmd/fabric-ca-client/identity.go @@ -82,9 +82,7 @@ func (c *ClientCmd) newAddIdentityCommand() *cobra.Command { RunE: c.runAddIdentity, } flags := identityAddCmd.Flags() - util.RegisterFlags(c.myViper, flags, &c.dynamicIdentity.add.IdentityInfo, nil) - flags.StringVarP( - &c.dynamicIdentity.add.Secret, "secret", "", "", "The enrollment secret for the identity being registered") + util.RegisterFlags(c.myViper, flags, &c.dynamicIdentity.add, nil) flags.StringSliceVarP( &c.cfgAttrs, "attrs", "", nil, "A list of comma-separated attributes of the form = (e.g. foo=foo1,bar=bar1)") flags.StringVarP( @@ -105,9 +103,7 @@ func (c *ClientCmd) newModifyIdentityCommand() *cobra.Command { tags := map[string]string{ "skip.id": "true", } - util.RegisterFlags(c.myViper, flags, &c.dynamicIdentity.modify.IdentityInfo, tags) - flags.StringVarP( - &c.dynamicIdentity.modify.Secret, "secret", "", "", "The enrollment secret for the identity being registered") + util.RegisterFlags(c.myViper, flags, &c.dynamicIdentity.modify, tags) flags.StringSliceVarP( &c.cfgAttrs, "attrs", "", nil, "A list of comma-separated attributes of the form = (e.g. foo=foo1,bar=bar1)") flags.StringVarP( @@ -145,7 +141,7 @@ func (c *ClientCmd) runListIdentity(cmd *cobra.Command, args []string) error { return err } - fmt.Printf("%+v\n", resp.IdentityInfo) + fmt.Printf("Name: %s, Type: %s, Affiliation: %s, Max Enrollments: %d, Attributes: %+v\n", resp.ID, resp.Type, resp.Affiliation, resp.MaxEnrollments, resp.Attributes) return nil } @@ -172,28 +168,23 @@ func (c *ClientCmd) runAddIdentity(cmd *cobra.Command, args []string) error { req := &api.AddIdentityRequest{} if c.dynamicIdentity.json != "" { - newIdentity := api.IdentityInfo{} - err := util.Unmarshal([]byte(c.dynamicIdentity.json), &newIdentity, "addIdentity") + err := util.Unmarshal([]byte(c.dynamicIdentity.json), &req, "addIdentity") if err != nil { return errors.Wrap(err, "Invalid value for --json option") } - req.IdentityInfo = newIdentity } else { - req.IdentityInfo = c.dynamicIdentity.add.IdentityInfo - req.IdentityInfo.Attributes = c.clientCfg.ID.Attributes + req = &c.dynamicIdentity.add + req.Attributes = c.clientCfg.ID.Attributes } req.ID = args[0] req.CAName = c.clientCfg.CAName - req.Secret = c.dynamicIdentity.add.Secret resp, err := id.AddIdentity(req) if err != nil { return err } - fmt.Printf("Successfully added identity: %+v\n", resp.IdentityInfo) - fmt.Printf("Secret: %s\n", resp.Secret) - + fmt.Printf("Successfully added identity - Name: %s, Type: %s, Affiliation: %s, Max Enrollments: %d, Secret: %s, Attributes: %+v\n", resp.ID, resp.Type, resp.Affiliation, resp.MaxEnrollments, resp.Secret, resp.Attributes) return nil } @@ -212,15 +203,13 @@ func (c *ClientCmd) runModifyIdentity(cmd *cobra.Command, args []string) error { } if c.dynamicIdentity.json != "" { - modifyIdentity := &api.IdentityInfo{} - err := util.Unmarshal([]byte(c.dynamicIdentity.json), modifyIdentity, "modifyIdentity") + err := util.Unmarshal([]byte(c.dynamicIdentity.json), req, "modifyIdentity") if err != nil { return errors.Wrap(err, "Invalid value for --json option") } - req.IdentityInfo = *modifyIdentity } else { - req.IdentityInfo = c.dynamicIdentity.modify.IdentityInfo - req.IdentityInfo.Attributes = c.clientCfg.ID.Attributes + req = &c.dynamicIdentity.modify + req.Attributes = c.clientCfg.ID.Attributes } req.ID = args[0] @@ -230,8 +219,7 @@ func (c *ClientCmd) runModifyIdentity(cmd *cobra.Command, args []string) error { return err } - fmt.Printf("Successfully modified identity: %+v\n", resp.IdentityInfo) - + fmt.Printf("Successfully modified identity - Name: %s, Type: %s, Affiliation: %s, Max Enrollments: %d, Secret: %s, Attributes: %+v\n", resp.ID, resp.Type, resp.Affiliation, resp.MaxEnrollments, resp.Secret, resp.Attributes) return nil } @@ -252,8 +240,7 @@ func (c *ClientCmd) runRemoveIdentity(cmd *cobra.Command, args []string) error { return err } - fmt.Printf("Successfully removed identity: %+v\n", resp.IdentityInfo) - + fmt.Printf("Successfully removed identity - Name: %s, Type: %s, Affiliation: %s, Max Enrollments: %d, Attributes: %+v\n", resp.ID, resp.Type, resp.Affiliation, resp.MaxEnrollments, resp.Attributes) return nil } diff --git a/cmd/fabric-ca-client/main_test.go b/cmd/fabric-ca-client/main_test.go index 4f334e1b8..2bed18a92 100644 --- a/cmd/fabric-ca-client/main_test.go +++ b/cmd/fabric-ca-client/main_test.go @@ -684,6 +684,15 @@ func TestIdentityCmd(t *testing.T) { cmdName, "identity", "add", "testuser1", "--json", `{"secret": "user1pw", "type": "user", "affiliation": "org1", "max_enrollments": 1, "attrs": [{"name:": "hf.Revoker", "value": "false"}]}`}) assert.Error(t, err, "Should have failed to add same user twice") + // Check that the secret got correctly configured + err = RunMain([]string{ + cmdName, "enroll", "-u", "http://testuser1:user1pw@localhost:7090", "-d"}) + assert.NoError(t, err, "Failed to enroll user 'testuser2'") + + // Enroll admin back to use it credentials for next commands + err = RunMain([]string{cmdName, "enroll", "-u", enrollURL}) + util.FatalError(t, err, "Failed to enroll user") + // Add user using flags err = RunMain([]string{ cmdName, "identity", "add", "testuser2", "--secret", "user2pw", "--type", "user", "--affiliation", ".", "--maxenrollments", "1", "--attrs", "hf.Revoker=true"}) @@ -698,6 +707,20 @@ func TestIdentityCmd(t *testing.T) { err = RunMain([]string{cmdName, "enroll", "-u", enrollURL}) util.FatalError(t, err, "Failed to enroll user") + // modify user secret using flags + err = RunMain([]string{ + cmdName, "identity", "modify", "testuser2", "--secret", "user2pw2"}) + assert.NoError(t, err, "Failed to add user 'testuser2'") + + // Check that the secret got correctly configured + err = RunMain([]string{ + cmdName, "enroll", "-u", "http://testuser2:user2pw2@localhost:7090", "-d"}) + assert.NoError(t, err, "Failed to enroll user 'testuser2'") + + // Enroll admin back to use it credentials for next commands + err = RunMain([]string{cmdName, "enroll", "-u", enrollURL}) + util.FatalError(t, err, "Failed to enroll user") + registry := server.CA.DBAccessor() user, err := registry.GetUser("testuser1", nil) assert.NoError(t, err, "Failed to get user 'testuser1'") @@ -763,7 +786,7 @@ func TestAffiliationCmd(t *testing.T) { result, err := captureOutput(RunMain, []string{cmdName, "affiliation", "list"}) assert.NoError(t, err, "Failed to return all affiliations") - assert.Equal(t, "org1\n", result) + assert.Equal(t, "affiliation: org1\n", result) err = RunMain([]string{cmdName, "affiliation", "list", "--affiliation", "org2"}) assert.Error(t, err, "Should failed to get the requested affiliation, affiliation does not exist") diff --git a/cmd/fabric-ca-server/config.go b/cmd/fabric-ca-server/config.go index 7962a6c81..daa846784 100644 --- a/cmd/fabric-ca-server/config.go +++ b/cmd/fabric-ca-server/config.go @@ -234,7 +234,7 @@ ldap: converters: - name: value: - # The 'maps' section contains named maps which may be referenced by the 'map' + # The 'maps' section contains named maps which may be referenced by the 'map' # function in the 'converters' section to map LDAP responses to arbitrary values. # For example, assume a user has an LDAP attribute named 'member' which has multiple # values which are each a distinguished name (i.e. a DN). For simplicity, assume the diff --git a/lib/client.go b/lib/client.go index fef1475c4..1e6704eaa 100644 --- a/lib/client.go +++ b/lib/client.go @@ -135,6 +135,8 @@ type GetServerInfoResponse struct { // CAChain is the PEM-encoded bytes of the fabric-ca-server's CA chain. // The 1st element of the chain is the root CA cert CAChain []byte + // Version of the server + Version string } // GetCAInfo returns generic CA information @@ -172,6 +174,7 @@ func (c *Client) net2LocalServerInfo(net *serverInfoResponseNet, local *GetServe } local.CAName = net.CAName local.CAChain = caChain + local.Version = net.Version return nil } diff --git a/lib/dbaccessor.go b/lib/dbaccessor.go index bb4f4bdec..ebc468932 100644 --- a/lib/dbaccessor.go +++ b/lib/dbaccessor.go @@ -20,6 +20,8 @@ import ( "encoding/json" "strings" + "github.com/hyperledger/fabric-ca/lib/attr" + "github.com/pkg/errors" "github.com/cloudflare/cfssl/log" @@ -467,6 +469,56 @@ func (d *Accessor) GetAffiliation(name string) (spi.Affiliation, error) { return affiliation, nil } +// GetAffiliationTree returns the requested affiliation and affiliations below +func (d *Accessor) GetAffiliationTree(name string) (*spi.DbTxResult, error) { + log.Debugf("DB: Get affiliation tree for '%s'", name) + + if name != "" { + _, err := d.GetAffiliation(name) + if err != nil { + return nil, err + } + } + + result, err := d.doTransaction(d.getAffiliationTreeTx, name) + if err != nil { + return nil, err + } + + getResult := result.(*spi.DbTxResult) + + return getResult, nil +} + +// GetAffiliation gets affiliation from database +func (d *Accessor) getAffiliationTreeTx(tx *sqlx.Tx, args ...interface{}) (interface{}, error) { + name := args[0].(string) + + log.Debugf("DB: Get affiliation tree for %s", name) + err := d.checkDB() + if err != nil { + return nil, err + } + + // Getting affiliations + allAffs := []AffiliationRecord{} + if name == "" { // Requesting all affiliations + err = tx.Select(&allAffs, tx.Rebind("SELECT * FROM affiliations")) + if err != nil { + return nil, newHTTPErr(500, ErrGettingAffiliation, "Failed to get affiliation tree for '%s': %s", name, err) + } + } else { + err = tx.Select(&allAffs, tx.Rebind("Select * FROM affiliations where (name LIKE ?) OR (name = ?)"), name+".%", name) + if err != nil { + return nil, newHTTPErr(500, ErrGettingAffiliation, "Failed to get affiliation tree for '%s': %s", name, err) + } + } + + ids := []UserRecord{} // TODO: Return identities associated with these affiliations + result := d.getResult(ids, allAffs) + return result, nil +} + // GetProperties returns the properties from the database func (d *Accessor) GetProperties(names []string) (map[string]string, error) { log.Debugf("DB: Get properties %s", names) @@ -645,11 +697,11 @@ func (d *Accessor) modifyAffiliationTx(tx *sqlx.Tx, args ...interface{}) (interf allOldAffiliations = append(allOldAffiliations, oldAffiliationRecord) log.Debugf("Affiliations to be modified %+v", allOldAffiliations) - var idsWithOldAff []UserRecord - var allIds []UserRecord // Iterate through all the affiliations found and update to use new affiliation path + idsUpdated := []string{} for _, affiliation := range allOldAffiliations { + var idsWithOldAff []UserRecord oldPath := affiliation.Name oldParentPath := affiliation.Prekey newPath := strings.Replace(oldPath, oldAffiliation, newAffiliation, 1) @@ -662,7 +714,6 @@ func (d *Accessor) modifyAffiliationTx(tx *sqlx.Tx, args ...interface{}) (interf if err != nil { return nil, err } - if len(idsWithOldAff) > 0 { if !isRegistar { return nil, newAuthErr(ErrMissingRegAttr, "Modifying affiliation affects identities, but caller is not a registrar") @@ -685,11 +736,53 @@ func (d *Accessor) modifyAffiliationTx(tx *sqlx.Tx, args ...interface{}) (interf if err != nil { return nil, errors.Wrapf(err, "Failed to execute query '%s' for multiple certificate removal", query) } + + // If user's affiliation is being updated, need to also update 'hf.Affiliation' attribute of user + for _, userRec := range idsWithOldAff { + user := d.newDBUser(&userRec) + currentAttrs, _ := user.GetAttributes(nil) // Get all current user attributes + userAff := GetUserAffiliation(user) // Get the current affiliation + newAff := strings.Replace(userAff, oldAffiliation, newAffiliation, 1) // Replace old affiliation with new affiliation + userAttrs := getNewAttributes(currentAttrs, []api.Attribute{ // Generate the new set of attributes for user + api.Attribute{ + Name: attr.Affiliation, + Value: newAff, + }, + }) + + attrBytes, err := json.Marshal(userAttrs) + if err != nil { + return nil, err + } + + // Update attributes + query := "UPDATE users SET attributes = ? where (id = ?)" + id := user.GetName() + res, err := tx.Exec(tx.Rebind(query), string(attrBytes), id) + if err != nil { + return nil, err + } + + numRowsAffected, err := res.RowsAffected() + if err != nil { + return nil, errors.Wrap(err, "Failed to get number of rows affected") + } + + if numRowsAffected == 0 { + return nil, errors.Errorf("No rows were affected when updating the state of identity %s", id) + } + + if numRowsAffected != 1 { + return nil, errors.Errorf("%d rows were affected when updating the state of identity %s", numRowsAffected, id) + } + } } else { // If force option is not specified, can only modify affiliation if there are no identities that have that affiliation idNamesStr := strings.Join(ids, ",") return nil, newHTTPErr(400, ErrUpdateConfigModifyAff, "The request to modify affiliation '%s' has the following identities associated: %s. Need to use 'force' to remove identities and affiliation", oldAffiliation, idNamesStr) } + + idsUpdated = append(idsUpdated, ids...) } // Update the affiliation record in the database to use new affiliation path @@ -702,14 +795,30 @@ func (d *Accessor) modifyAffiliationTx(tx *sqlx.Tx, args ...interface{}) (interf if numRowsAffected == 0 { return nil, errors.Errorf("Failed to update any affiliation records for '%s'", oldPath) } + } - for _, id := range idsWithOldAff { - allIds = append(allIds, id) + // Generate the result set that has all identities with their new affiliation and all renamed affiliations + var idsWithNewAff []UserRecord + if len(idsUpdated) > 0 { + query = "Select * FROM users WHERE (id IN (?))" + inQuery, args, err := sqlx.In(query, idsUpdated) + if err != nil { + return nil, errors.Wrapf(err, "Failed to construct query '%s'", query) } + err = tx.Select(&idsWithNewAff, tx.Rebind(inQuery), args...) + if err != nil { + return nil, errors.Wrapf(err, "Failed to execute query '%s' for getting users with new affiliation", query) + } + } + + allNewAffs := []AffiliationRecord{} + err = tx.Select(&allNewAffs, tx.Rebind("Select * FROM affiliations where (name LIKE ?) OR (name = ?)"), newAffiliation+".%", newAffiliation) + if err != nil { + return nil, newHTTPErr(500, ErrGettingAffiliation, "Failed to get affiliation tree for '%s': %s", newAffiliation, err) } // Return the identities and affiliations that were modified - result := d.getResult(allIds, allOldAffiliations) + result := d.getResult(idsWithNewAff, allNewAffs) return result, nil } diff --git a/lib/identity.go b/lib/identity.go index 997e09c8c..fb55da720 100644 --- a/lib/identity.go +++ b/lib/identity.go @@ -315,20 +315,21 @@ func (i *Identity) GetAffiliation(affiliation, caname string) (*api.AffiliationR } // GetAllAffiliations returns all affiliations that the caller is authorized to see -func (i *Identity) GetAllAffiliations(caname string, cb func(*json.Decoder) error) error { +func (i *Identity) GetAllAffiliations(caname string) (*api.AffiliationResponse, error) { log.Debugf("Entering identity.GetAllAffiliations") - err := i.GetStreamResponse("affiliations", caname, "result.affiliations", cb) + result := &api.AffiliationResponse{} + err := i.Get("affiliations", caname, result) if err != nil { - return err + return nil, err } log.Debug("Successfully retrieved affiliations") - return nil + return result, nil } // AddAffiliation adds a new affiliation to the server func (i *Identity) AddAffiliation(req *api.AddAffiliationRequest) (*api.AffiliationResponse, error) { log.Debugf("Entering identity.AddAffiliation with request: %+v", req) - if req.Info.Name == "" { + if req.Name == "" { return nil, errors.New("Affiliation to add was not specified") } @@ -351,14 +352,14 @@ func (i *Identity) AddAffiliation(req *api.AddAffiliationRequest) (*api.Affiliat } // ModifyAffiliation renames an existing affiliation on the server -func (i *Identity) ModifyAffiliation(req *api.ModifyAffiliationRequest) (*api.AffiliationWithIdentityResponse, error) { +func (i *Identity) ModifyAffiliation(req *api.ModifyAffiliationRequest) (*api.AffiliationResponse, error) { log.Debugf("Entering identity.ModifyAffiliation with request: %+v", req) modifyAff := req.Name if modifyAff == "" { return nil, errors.New("Affiliation to modify was not specified") } - if req.Info.Name == "" { + if req.NewName == "" { return nil, errors.New("New affiliation not specified") } @@ -368,7 +369,7 @@ func (i *Identity) ModifyAffiliation(req *api.ModifyAffiliationRequest) (*api.Af } // Send a put to the "affiliations" endpoint with req as body - result := &api.AffiliationWithIdentityResponse{} + result := &api.AffiliationResponse{} queryParam := make(map[string]string) queryParam["force"] = strconv.FormatBool(req.Force) err = i.Put(fmt.Sprintf("affiliations/%s", modifyAff), reqBody, queryParam, result) @@ -381,7 +382,7 @@ func (i *Identity) ModifyAffiliation(req *api.ModifyAffiliationRequest) (*api.Af } // RemoveAffiliation removes an existing affiliation from the server -func (i *Identity) RemoveAffiliation(req *api.RemoveAffiliationRequest) (*api.AffiliationWithIdentityResponse, error) { +func (i *Identity) RemoveAffiliation(req *api.RemoveAffiliationRequest) (*api.AffiliationResponse, error) { log.Debugf("Entering identity.RemoveAffiliation with request: %+v", req) removeAff := req.Name if removeAff == "" { @@ -389,7 +390,7 @@ func (i *Identity) RemoveAffiliation(req *api.RemoveAffiliationRequest) (*api.Af } // Send a delete to the "affiliations" endpoint with the affiliation as a path parameter - result := &api.AffiliationWithIdentityResponse{} + result := &api.AffiliationResponse{} queryParam := make(map[string]string) queryParam["force"] = strconv.FormatBool(req.Force) queryParam["ca"] = req.CAName diff --git a/lib/ldap/client.go b/lib/ldap/client.go index 526fe05b5..75b89c1d0 100644 --- a/lib/ldap/client.go +++ b/lib/ldap/client.go @@ -304,6 +304,11 @@ func (lc *Client) GetFilteredUsers(affiliation, types string) (*sqlx.Rows, error return nil, errNotSupported } +// GetAffiliationTree returns the requested affiliations and all affiliations below it +func (lc *Client) GetAffiliationTree(name string) (*spi.DbTxResult, error) { + return nil, errNotSupported +} + // Connect to the LDAP server and bind as user as admin user as specified in LDAP URL func (lc *Client) newConnection() (conn *ldap.Conn, err error) { address := fmt.Sprintf("%s:%d", lc.Host, lc.Port) diff --git a/lib/serveraffiliations.go b/lib/serveraffiliations.go index 2dca964b7..e07ee60d2 100644 --- a/lib/serveraffiliations.go +++ b/lib/serveraffiliations.go @@ -18,8 +18,6 @@ package lib import ( "fmt" - "net/http" - "os" "strconv" "strings" @@ -27,7 +25,6 @@ import ( "github.com/hyperledger/fabric-ca/api" "github.com/hyperledger/fabric-ca/lib/attr" "github.com/hyperledger/fabric-ca/lib/spi" - "github.com/hyperledger/fabric-ca/util" "github.com/pkg/errors" ) @@ -65,6 +62,10 @@ func affiliationsHandler(ctx *serverRequestContext) (interface{}, error) { if err != nil { return nil, err } + err = ctx.HasRole(attr.AffiliationMgr) + if err != nil { + return nil, err + } // Process Request resp, err := processAffiliationRequest(ctx, caname, caller) if err != nil { @@ -91,6 +92,10 @@ func affiliationsStreamingHandler(ctx *serverRequestContext) (interface{}, error if err != nil { return nil, err } + err = ctx.HasRole(attr.AffiliationMgr) + if err != nil { + return nil, err + } // Process Request resp, err := processStreamingAffiliationRequest(ctx, caname, caller) if err != nil { @@ -107,7 +112,7 @@ func processStreamingAffiliationRequest(ctx *serverRequestContext, caname string method := ctx.req.Method switch method { case "GET": - return nil, processGetAllAffiliationsRequest(ctx, caller, caname) + return processGetAllAffiliationsRequest(ctx, caller, caname) case "POST": return processAffiliationPostRequest(ctx, caname) default: @@ -132,18 +137,18 @@ func processAffiliationRequest(ctx *serverRequestContext, caname string, caller } } -func processGetAllAffiliationsRequest(ctx *serverRequestContext, caller spi.User, caname string) error { +func processGetAllAffiliationsRequest(ctx *serverRequestContext, caller spi.User, caname string) (*api.AffiliationResponse, error) { log.Debug("Processing GET all affiliations request") - err := getAffiliations(ctx, caller, caname) + resp, err := getAffiliations(ctx, caller, caname) if err != nil { - return err + return nil, err } - return nil + return resp, nil } -func processGetAffiliationRequest(ctx *serverRequestContext, caller spi.User, caname string) (interface{}, error) { +func processGetAffiliationRequest(ctx *serverRequestContext, caller spi.User, caname string) (*api.AffiliationResponse, error) { log.Debug("Processing GET affiliation request") affiliation, err := ctx.GetVar("affiliation") @@ -159,110 +164,64 @@ func processGetAffiliationRequest(ctx *serverRequestContext, caller spi.User, ca return resp, nil } -func getAffiliations(ctx *serverRequestContext, caller spi.User, caname string) error { +func getAffiliations(ctx *serverRequestContext, caller spi.User, caname string) (*api.AffiliationResponse, error) { log.Debug("Requesting all affiliations that the caller is authorized view") var err error - w := ctx.resp - flusher, _ := w.(http.Flusher) - - err = ctx.HasRole(attr.AffiliationMgr) - if err != nil { - return err - } - - // Get the number of identities to return back to client in a chunk based on the environment variable - // If environment variable not set, default to 100 identities - numberOfAffiliations := os.Getenv("FABRIC_CA_SERVER_MAX_AFFILIATIONS_PER_CHUNK") - var numAffiliations int - if numberOfAffiliations == "" { - numAffiliations = 100 - } else { - numAffiliations, err = strconv.Atoi(numberOfAffiliations) - if err != nil { - return newHTTPErr(500, ErrGettingAffiliation, "Incorrect format specified for environment variable 'FABRIC_CA_SERVER_MAX_AFFILIATIONS_PER_CHUNK', an integer value is required: %s", err) - } - } - registry := ctx.ca.registry callerAff := GetUserAffiliation(caller) rows, err := registry.GetAllAffiliations(callerAff) if err != nil { - return newHTTPErr(500, ErrGettingUser, "Failed to get affiliation: %s", err) + return nil, newHTTPErr(500, ErrGettingUser, "Failed to get affiliation: %s", err) } - w.Write([]byte(`{"affiliations":[`)) - - rowNumber := 0 + an := &affiliationNode{} for rows.Next() { - rowNumber++ var aff AffiliationRecord err := rows.StructScan(&aff) if err != nil { - return newHTTPErr(500, ErrGettingAffiliation, "Failed to get read row: %s", err) - } - - if rowNumber > 1 { - w.Write([]byte(",")) - } - - affInfo := api.AffiliationInfo{ - Name: aff.Name, - } - - resp, err := util.Marshal(affInfo, "identities info") - if err != nil { - return newHTTPErr(500, ErrGettingUser, "Failed to marshal identity info: %s", err) + return nil, newHTTPErr(500, ErrGettingAffiliation, "Failed to get read row: %s", err) } - w.Write(resp) - // If hit the number of identities requested then flush - if rowNumber%numAffiliations == 0 { - flusher.Flush() // Trigger "chunked" encoding and send a chunk... - } + an.insertByName(aff.Name) } + root := an.GetRoot() - // Close the JSON object - w.Write([]byte(fmt.Sprintf("], \"caname\":\"%s\"}", caname))) - flusher.Flush() + resp := &api.AffiliationResponse{ + CAName: caname, + } + resp.Name = root.Name + resp.Affiliations = root.Affiliations + resp.Identities = root.Identities - return nil + return resp, nil } func getAffiliation(ctx *serverRequestContext, caller spi.User, requestedAffiliation, caname string) (*api.AffiliationResponse, error) { log.Debugf("Requesting affiliation '%s'", requestedAffiliation) - err := ctx.HasRole(attr.AffiliationMgr) + registry := ctx.ca.registry + err := ctx.ContainsAffiliation(requestedAffiliation) if err != nil { return nil, err } - registry := ctx.ca.registry - err = ctx.ContainsAffiliation(requestedAffiliation) + result, err := registry.GetAffiliationTree(requestedAffiliation) if err != nil { - return nil, err + return nil, newHTTPErr(500, ErrGettingAffiliation, "Failed to get affiliation: %s", err) } - affiliation, err := registry.GetAffiliation(requestedAffiliation) + + resp, err := getResponse(result, caname) if err != nil { return nil, err } - resp := &api.AffiliationResponse{ - CAName: caname, - } - resp.Info.Name = affiliation.GetName() - return resp, nil } -func processAffiliationDeleteRequest(ctx *serverRequestContext, caname string) (*api.AffiliationWithIdentityResponse, error) { +func processAffiliationDeleteRequest(ctx *serverRequestContext, caname string) (*api.AffiliationResponse, error) { log.Debug("Processing DELETE request") - err := ctx.HasRole(attr.AffiliationMgr) - if err != nil { - return nil, err - } - if !ctx.ca.Config.Cfg.Affiliations.AllowRemove { return nil, newAuthErr(ErrUpdateConfigRemoveAff, "Affiliation removal is disabled") } @@ -315,18 +274,13 @@ func processAffiliationPostRequest(ctx *serverRequestContext, caname string) (*a ctx.endpoint.successRC = 201 - err := ctx.HasRole(attr.AffiliationMgr) - if err != nil { - return nil, err - } - var req api.AddAffiliationRequestNet - err = ctx.ReadBody(&req) + err := ctx.ReadBody(&req) if err != nil { return nil, err } - addAffiliation := req.Info.Name + addAffiliation := req.Name log.Debugf("Request to add affiliation '%s'", addAffiliation) registry := ctx.ca.registry @@ -386,15 +340,13 @@ func processAffiliationPostRequest(ctx *serverRequestContext, caname string) (*a } - resp := &api.AffiliationResponse{ - CAName: caname, - } - resp.Info.Name = addAffiliation + resp := &api.AffiliationResponse{CAName: caname} + resp.Name = addAffiliation return resp, nil } -func processAffiliationPutRequest(ctx *serverRequestContext, caname string) (*api.AffiliationWithIdentityResponse, error) { +func processAffiliationPutRequest(ctx *serverRequestContext, caname string) (*api.AffiliationResponse, error) { log.Debug("Processing PUT request") modifyAffiliation, err := ctx.GetVar("affiliation") @@ -407,7 +359,7 @@ func processAffiliationPutRequest(ctx *serverRequestContext, caname string) (*ap if err != nil { return nil, err } - newAffiliation := req.Info.Name + newAffiliation := req.NewName log.Debugf("Request to modify affiliation '%s' to '%s'", modifyAffiliation, newAffiliation) err = ctx.ContainsAffiliation(modifyAffiliation) @@ -452,25 +404,167 @@ func processAffiliationPutRequest(ctx *serverRequestContext, caname string) (*ap return resp, nil } -func getResponse(result *spi.DbTxResult, caname string) (*api.AffiliationWithIdentityResponse, error) { - affInfo := []api.AffiliationInfo{} - for _, aff := range result.Affiliations { - info := &api.AffiliationInfo{ - Name: aff.GetName(), +func getResponse(result *spi.DbTxResult, caname string) (*api.AffiliationResponse, error) { + resp := &api.AffiliationResponse{CAName: caname} + // Get all root affiliation names from the result + rootNames := getRootAffiliationNames(result.Affiliations) + if len(rootNames) == 0 { + return resp, nil + } + if len(rootNames) != 1 { + return nil, errors.Errorf("multiple root affiliations found: %+v", rootNames) + } + affInfo := &api.AffiliationInfo{} + err := fillAffiliationInfo(affInfo, rootNames[0], result, result.Affiliations) + if err != nil { + return nil, err + } + resp.AffiliationInfo = *affInfo + return resp, nil +} + +// Get all of the root affiliation names from this list of affiliations +func getRootAffiliationNames(affiliations []spi.Affiliation) []string { + roots := []string{} + for _, aff1 := range affiliations { + isRoot := true + for _, aff2 := range affiliations { + if isChildAffiliation(aff2.GetName(), aff1.GetName()) { + isRoot = false + break + } + } + if isRoot { + roots = append(roots, aff1.GetName()) } - affInfo = append(affInfo, *info) } - idInfo := []api.IdentityInfo{} + return roots +} + +// Fill 'info' with affiliation info associated with affiliation 'name' hierarchically. +// Use 'affiliations' to find child affiliations for this affiliation, and +// 'identities' to find identities associated with this affiliation. +func fillAffiliationInfo(info *api.AffiliationInfo, name string, result *spi.DbTxResult, affiliations []spi.Affiliation) error { + info.Name = name + // Add identities which have this affiliation + identities := []api.IdentityInfo{} for _, identity := range result.Identities { - id, err := getIDInfo(identity) + idAff := strings.Join(identity.GetAffiliationPath(), ".") + if idAff == name { + id, err := getIDInfo(identity) + if err != nil { + return err + } + identities = append(identities, *id) + } + } + if len(identities) > 0 { + info.Identities = identities + } + // Create child affiliations (if any) + children := []api.AffiliationInfo{} + var child spi.Affiliation + for { + child = nil + // Search for a child affiliations + for idx, aff := range affiliations { + affName := aff.GetName() + if isChildAffiliation(name, affName) { + child = aff + // Remove this child affiliation + affiliations = append(affiliations[:idx], affiliations[idx+1:]...) + break + } + } + if child == nil { + // No more children of this affiliation 'name' found + break + } + // Found a child of affiliation 'name' + childAff := api.AffiliationInfo{Name: child.GetName()} + err := fillAffiliationInfo(&childAff, child.GetName(), result, affiliations) if err != nil { - return nil, err + return err } - idInfo = append(idInfo, *id) + children = append(children, childAff) + } + if len(children) > 0 { + info.Affiliations = children + } + return nil +} + +// Determine if the affiliation with name 'child' is a child of affiliation with name 'name' +func isChildAffiliation(name, child string) bool { + if !strings.HasPrefix(child, name+".") { + return false + } + nameParts := strings.Split(name, ".") + childParts := strings.Split(child, ".") + if len(childParts) != len(nameParts)+1 { + return false } - return &api.AffiliationWithIdentityResponse{ - Affiliations: affInfo, - Identities: idInfo, - CAName: caname, + return true +} + +func getIDInfo(user spi.User) (*api.IdentityInfo, error) { + allAttributes, err := user.GetAttributes(nil) + if err != nil { + return nil, err + } + return &api.IdentityInfo{ + ID: user.GetName(), + Type: user.GetType(), + Affiliation: GetUserAffiliation(user), + Attributes: allAttributes, + MaxEnrollments: user.GetMaxEnrollments(), }, nil } + +type affiliationNode struct { + children map[string]*affiliationNode +} + +func (an *affiliationNode) insertByName(name string) { + an.insertByPath(strings.Split(name, ".")) +} + +func (an *affiliationNode) insertByPath(path []string) { + if len(path) == 0 { + return + } + if an.children == nil { + an.children = map[string]*affiliationNode{} + } + node := an.children[path[0]] + if node == nil { + node = &affiliationNode{} + an.children[path[0]] = node + } + node.insertByPath(path[1:]) +} + +func (an *affiliationNode) GetRoot() *api.AffiliationInfo { + result := &api.AffiliationInfo{} + an.fill([]string{}, result) + switch len(result.Affiliations) { + case 0: + return nil + case 1: + return &result.Affiliations[0] + default: + return result + } +} + +func (an *affiliationNode) fill(path []string, ai *api.AffiliationInfo) { + ai.Name = strings.Join(path, ".") + if len(an.children) > 0 { + ai.Affiliations = make([]api.AffiliationInfo, len(an.children)) + idx := 0 + for key, child := range an.children { + child.fill(append(path, key), &ai.Affiliations[idx]) + idx++ + } + } +} diff --git a/lib/serveraffiliations_test.go b/lib/serveraffiliations_test.go index 670271477..40fccab2d 100644 --- a/lib/serveraffiliations_test.go +++ b/lib/serveraffiliations_test.go @@ -16,8 +16,8 @@ limitations under the License. package lib import ( + "fmt" "os" - "strings" "testing" "golang.org/x/crypto/ocsp" @@ -55,7 +55,7 @@ func TestGetAllAffiliations(t *testing.T) { admin2 := resp.Identity - result, err := captureOutput(admin.GetAllAffiliations, "", AffiliationDecoder) + getResp, err := admin.GetAllAffiliations("") assert.NoError(t, err, "Failed to get all affiliations") affiliations := []AffiliationRecord{} @@ -64,18 +64,19 @@ func TestGetAllAffiliations(t *testing.T) { t.Error("Failed to get all affiliations in database") } + fmt.Println("affiliations: ", affiliations) for _, aff := range affiliations { - if !strings.Contains(result, aff.Name) { + if !searchTree(getResp, aff.Name) { t.Error("Failed to get all appropriate affiliations") } } // admin2's affilations is "org2" - result, err = captureOutput(admin2.GetAllAffiliations, "", AffiliationDecoder) + getResp, err = admin2.GetAllAffiliations("") assert.NoError(t, err, "Failed to get all affiliations for admin2") - if !strings.Contains(result, "org2") { - t.Error("Incorrect affiliation received") + if !searchTree(getResp, "org2") { + t.Error("Failed to get all appropriate affiliations") } notAffMgr, err := admin.RegisterAndEnroll(&api.RegistrationRequest{ @@ -83,11 +84,10 @@ func TestGetAllAffiliations(t *testing.T) { }) util.FatalError(t, err, "Failed to register a user that is not affiliation manager") - err = notAffMgr.GetAllAffiliations("", AffiliationDecoder) + _, err = notAffMgr.GetAllAffiliations("") if assert.Error(t, err, "Should have failed, as the caller does not have the attribute 'hf.AffiliationMgr'") { assert.Contains(t, err.Error(), "User does not have attribute 'hf.AffiliationMgr'") } - } func TestGetAffiliation(t *testing.T) { @@ -118,9 +118,14 @@ func TestGetAffiliation(t *testing.T) { admin2 := resp.Identity - getAffResp, err := admin.GetAffiliation("org2.dept1", "") + getAffResp, err := admin.GetAffiliation("org2", "") + assert.NoError(t, err, "Failed to get requested affiliations") + assert.Equal(t, "org2", getAffResp.Name) + assert.Equal(t, "org2.dept1", getAffResp.Affiliations[0].Name) + + getAffResp, err = admin.GetAffiliation("org2.dept1", "") assert.NoError(t, err, "Failed to get requested affiliations") - assert.Equal(t, "org2.dept1", getAffResp.Info.Name) + assert.Equal(t, "org2.dept1", getAffResp.Name) getAffResp, err = admin2.GetAffiliation("org1", "") assert.Error(t, err, "Should have failed, caller not authorized to get affiliation") @@ -180,8 +185,9 @@ func TestDynamicAddAffiliation(t *testing.T) { admin2 := resp.Identity - addAffReq := &api.AddAffiliationRequest{} - addAffReq.Info.Name = "org3" + addAffReq := &api.AddAffiliationRequest{ + Name: "org3", + } addAffResp, err := notAffMgr.AddAffiliation(addAffReq) assert.Error(t, err, "Should have failed, caller does not have 'hf.AffiliationMgr' attribute") @@ -191,12 +197,12 @@ func TestDynamicAddAffiliation(t *testing.T) { addAffResp, err = admin.AddAffiliation(addAffReq) util.FatalError(t, err, "Failed to add affiliation 'org3'") - assert.Equal(t, "org3", addAffResp.Info.Name) + assert.Equal(t, "org3", addAffResp.Name) addAffResp, err = admin.AddAffiliation(addAffReq) assert.Error(t, err, "Should have failed affiliation 'org3' already exists") - addAffReq.Info.Name = "org3.dept1" + addAffReq.Name = "org3.dept1" addAffResp, err = admin.AddAffiliation(addAffReq) assert.NoError(t, err, "Failed to affiliation") @@ -204,7 +210,7 @@ func TestDynamicAddAffiliation(t *testing.T) { _, err = registry.GetAffiliation("org3.dept1") assert.NoError(t, err, "Failed to add affiliation correctly") - addAffReq.Info.Name = "org4.dept1.team2" + addAffReq.Name = "org4.dept1.team2" addAffResp, err = admin.AddAffiliation(addAffReq) assert.Error(t, err, "Should have failed, parent affiliation does not exist. Force option is required") @@ -217,7 +223,7 @@ func TestDynamicAddAffiliation(t *testing.T) { _, err = registry.GetAffiliation("org4.dept1") assert.NoError(t, err, "Failed to add affiliation correctly") - assert.Equal(t, "org4.dept1.team2", addAffResp.Info.Name) + assert.Equal(t, "org4.dept1.team2", addAffResp.Name) } func TestDynamicRemoveAffiliation(t *testing.T) { @@ -282,6 +288,11 @@ func TestDynamicRemoveAffiliation(t *testing.T) { }) assert.NoError(t, err, "Failed to register and enroll 'testuser1'") + _, err = admin.Register(&api.RegistrationRequest{ + Name: "testuser3", + Affiliation: "org2.dept1", + }) + _, err = registry.GetUser("testuser2", nil) assert.NoError(t, err, "User should exist") @@ -335,8 +346,9 @@ func TestDynamicRemoveAffiliation(t *testing.T) { t.Error("Failed to correctly revoke certificate for an identity whose affiliation was removed") } + assert.Equal(t, "org2", removeResp.Name) assert.Equal(t, "org2.dept1", removeResp.Affiliations[0].Name) - assert.Equal(t, "org2", removeResp.Affiliations[1].Name) + assert.Equal(t, "testuser3", removeResp.Affiliations[0].Identities[0].ID) assert.Equal(t, "admin2", removeResp.Identities[0].ID) _, err = admin.RemoveAffiliation(removeAffReq) @@ -375,10 +387,21 @@ func TestDynamicModifyAffiliation(t *testing.T) { }, }) + _, err = admin.AddAffiliation(&api.AddAffiliationRequest{ + Name: "org2.dept1.team1", + }) + assert.NoError(t, err, "Failed to add new affiliation") + + _, err = admin.Register(&api.RegistrationRequest{ + Name: "testuser2", + Affiliation: "org2.dept1.team1", + }) + assert.NoError(t, err, "Failed to register new user") + modifyAffReq := &api.ModifyAffiliationRequest{ - Name: "org2", + Name: "org2", + NewName: "org3", } - modifyAffReq.Info.Name = "org3" _, err = admin.ModifyAffiliation(modifyAffReq) assert.Error(t, err, "Should have failed, there is an identity associated with affiliation. Need to use force option") @@ -402,7 +425,39 @@ func TestDynamicModifyAffiliation(t *testing.T) { userAff := GetUserAffiliation(user) assert.Equal(t, "org3", userAff) - assert.Equal(t, "org2.dept1", modifyResp.Affiliations[0].Name) - assert.Equal(t, "org2", modifyResp.Affiliations[1].Name) + assert.Equal(t, "org3", modifyResp.Name) + assert.Equal(t, "org3.dept1", modifyResp.Affiliations[0].Name) assert.Equal(t, "testuser1", modifyResp.Identities[0].ID) } + +func TestAffiliationNode(t *testing.T) { + an := &affiliationNode{} + an.insertByName("a.b.c") + an.insertByName("a") + an.insertByName("a.c.b") + root := an.GetRoot() + assert.Equal(t, root.Name, "a") + assert.Equal(t, root.Affiliations[0].Name, "a.b") + assert.Equal(t, root.Affiliations[0].Affiliations[0].Name, "a.b.c") + assert.Equal(t, root.Affiliations[1].Name, "a.c") + assert.Equal(t, root.Affiliations[1].Affiliations[0].Name, "a.c.b") +} + +func searchTree(resp *api.AffiliationResponse, find string) bool { + if resp.Name == find { + return true + } + return searchChildren(resp.Affiliations, find) +} + +func searchChildren(children []api.AffiliationInfo, find string) bool { + for _, child := range children { + if child.Name == find { + return true + } + if searchChildren(child.Affiliations, find) { + return true + } + } + return false +} diff --git a/lib/serveridentities.go b/lib/serveridentities.go index b0fd45744..aa7c5e4a1 100644 --- a/lib/serveridentities.go +++ b/lib/serveridentities.go @@ -252,15 +252,19 @@ func getID(ctx *serverRequestContext, caller spi.User, id, caname string) (*api. return nil, err } - resp := &api.GetIDResponse{ - CAName: caname, - } - - idInfo, err := getIDInfo(user) + allAttributes, err := user.GetAttributes(nil) if err != nil { return nil, err } - resp.IdentityInfo = *idInfo + + resp := &api.GetIDResponse{ + ID: user.GetName(), + Type: user.GetType(), + Affiliation: GetUserAffiliation(user), + Attributes: allAttributes, + MaxEnrollments: user.GetMaxEnrollments(), + CAName: caname, + } return resp, nil } @@ -308,15 +312,10 @@ func processDeleteRequest(ctx *serverRequestContext, caname string) (*api.Identi return nil, newHTTPErr(500, ErrRemoveIdentity, "Failed to remove identity: ", err) } - resp := &api.IdentityResponse{ - CAName: caname, - } - - idInfo, err := getIDInfo(userToRemove) + resp, err := getIDResp(userToRemove, "", caname) if err != nil { return nil, err } - resp.IdentityInfo = *idInfo log.Debugf("Identity '%s' successfully removed", removeID) return resp, nil @@ -362,15 +361,10 @@ func processPostRequest(ctx *serverRequestContext, caname string) (*api.Identity return nil, err } - resp := &api.IdentityResponse{ - Secret: pass, - CAName: caname, - } - idInfo, err := getIDInfo(user) + resp, err := getIDResp(user, pass, caname) if err != nil { return nil, err } - resp.IdentityInfo = *idInfo log.Debugf("Identity successfully added") return resp, nil @@ -440,15 +434,10 @@ func processPutRequest(ctx *serverRequestContext, caname string) (*api.IdentityR return nil, err } - resp := &api.IdentityResponse{ - Secret: modReq.Pass, - CAName: caname, - } - idInfo, err := getIDInfo(userToModify) + resp, err := getIDResp(userToModify, req.Secret, caname) if err != nil { return nil, err } - resp.IdentityInfo = *idInfo log.Debugf("Identity successfully modified") return resp, nil @@ -496,17 +485,24 @@ func getModifyReq(user spi.User, req *api.ModifyIdentityRequest) (*spi.UserInfo, return &modifyUserInfo, setPass } -func getIDInfo(user spi.User) (*api.IdentityInfo, error) { +// Get the identity response +// Note that the secret will be the empty string unless the +// caller is permitted to see the secret. For example, +// when adding a new identity and a secret is automatically +// generated, it must be returned to the registrar. +func getIDResp(user spi.User, secret, caname string) (*api.IdentityResponse, error) { allAttributes, err := user.GetAttributes(nil) if err != nil { return nil, err } - return &api.IdentityInfo{ + return &api.IdentityResponse{ ID: user.GetName(), Type: user.GetType(), Affiliation: GetUserAffiliation(user), Attributes: allAttributes, MaxEnrollments: user.GetMaxEnrollments(), + Secret: secret, + CAName: caname, }, nil } diff --git a/lib/serverinfo.go b/lib/serverinfo.go index 087ee4cce..51cae4fbb 100644 --- a/lib/serverinfo.go +++ b/lib/serverinfo.go @@ -16,12 +16,18 @@ limitations under the License. package lib +import ( + "github.com/hyperledger/fabric-ca/lib/metadata" +) + // The response to the GET /info request type serverInfoResponseNet struct { // CAName is a unique name associated with fabric-ca-server's CA CAName string // Base64 encoding of PEM-encoded certificate chain CAChain string + // Version of the server + Version string } func newCAInfoEndpoint(s *Server) *serverEndpoint { @@ -43,5 +49,6 @@ func cainfoHandler(ctx *serverRequestContext) (interface{}, error) { if err != nil { return nil, err } + resp.Version = metadata.GetVersion() return resp, nil } diff --git a/lib/serverinfo_test.go b/lib/serverinfo_test.go new file mode 100644 index 000000000..f33e14708 --- /dev/null +++ b/lib/serverinfo_test.go @@ -0,0 +1,47 @@ +/* +Copyright IBM Corp. 2017, 2018 All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package lib + +import ( + "os" + "testing" + + "github.com/hyperledger/fabric-ca/api" + "github.com/hyperledger/fabric-ca/lib/metadata" + "github.com/hyperledger/fabric-ca/util" + "github.com/stretchr/testify/assert" +) + +func TestGetServerVersion(t *testing.T) { + os.RemoveAll(rootDir) + defer os.RemoveAll(rootDir) + + var err error + + metadata.Version = "1.1.0" + srv := TestGetRootServer(t) + err = srv.Start() + util.FatalError(t, err, "Failed to start server") + defer srv.Stop() + + client := getTestClient(7075) + resp, err := client.GetCAInfo(&api.GetCAInfoRequest{ + CAName: "", + }) + assert.NoError(t, err, "Failed to get back server info") + + assert.Equal(t, "1.1.0", resp.Version) +} diff --git a/lib/spi/userregistry.go b/lib/spi/userregistry.go index 7dac67f07..5407eee89 100644 --- a/lib/spi/userregistry.go +++ b/lib/spi/userregistry.go @@ -87,4 +87,5 @@ type UserRegistry interface { GetFilteredUsers(affiliation, types string) (*sqlx.Rows, error) DeleteAffiliation(name string, force, identityRemoval, isRegistrar bool) (*DbTxResult, error) ModifyAffiliation(oldAffiliation, newAffiliation string, force, isRegistrar bool) (*DbTxResult, error) + GetAffiliationTree(name string) (*DbTxResult, error) } diff --git a/scripts/fvt/version_test.sh b/scripts/fvt/version_test.sh index c30aa5c81..63b36e626 100755 --- a/scripts/fvt/version_test.sh +++ b/scripts/fvt/version_test.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/bash : ${TESTCASE="version"} FABRIC_CA="$GOPATH/src/github.com/hyperledger/fabric-ca" SCRIPTDIR="$FABRIC_CA/scripts/fvt" diff --git a/swagger/swagger-fabric-ca.json b/swagger/swagger-fabric-ca.json index 8068b9e91..66ec14e3f 100644 --- a/swagger/swagger-fabric-ca.json +++ b/swagger/swagger-fabric-ca.json @@ -931,19 +931,28 @@ "Result": { "type": "object", "properties": { + "name": { + "type": "string", + "description": "The affiliation path" + }, "affiliations": { "type": "array", + "description": "Contains any child affiliations, this continues until last affiliation is returned", "items": { "type": "object", "properties": { "name": { - "type": "string", - "description": "The affiliation path" + "type": "string", + "description": "The affiliation path" + }, + "affiliations": { + "type": "array", + "items": { + "type": "object", + "description": "The affiliation path" + } } - }, - "required": [ - "name" - ] + } } }, "caname": { @@ -1183,13 +1192,33 @@ "type": "string", "description": "The affiliation path" }, + "affiliations": { + "type": "array", + "description": "Contains any child affiliations, this continues until last affiliation is returned", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The affiliation path" + }, + "affiliations": { + "type": "array", + "items": { + "type": "object", + "description": "The affiliation path" + } + } + } + } + }, "caname": { "type": "string", - "description": "Name of the CA containing this affiliation." + "description": "Name of the CA containing these affiliations." } }, "required": [ - "name", + "affiliations", "caname" ] }, @@ -1315,19 +1344,28 @@ "Result": { "type": "object", "properties": { + "name": { + "type": "string", + "description": "The affiliation path" + }, "affiliations": { "type": "array", + "description": "Contains any child affiliations, this continues until last affiliation is returned", "items": { "type": "object", "properties": { "name": { - "type": "string", - "description": "The affiliation path" + "type": "string", + "description": "The affiliation path" + }, + "affiliations": { + "type": "array", + "items": { + "type": "object", + "description": "The affiliation path" + } } - }, - "required": [ - "name" - ] + } } }, "identities": { @@ -1489,19 +1527,28 @@ "Result": { "type": "object", "properties": { + "name": { + "type": "string", + "description": "The affiliation path" + }, "affiliations": { "type": "array", + "description": "Contains any child affiliations, this continues until last affiliation is returned", "items": { "type": "object", "properties": { "name": { - "type": "string", - "description": "The affiliation path" + "type": "string", + "description": "The affiliation path" + }, + "affiliations": { + "type": "array", + "items": { + "type": "object", + "description": "The affiliation path" + } } - }, - "required": [ - "name" - ] + } } }, "identities": { @@ -1819,7 +1866,7 @@ ], "description": "The maximum number of times that the secret can be used to enroll. \nIf 0, use the configured max_enrollments of the fabric-ca-server; \nIf > 0 and <= configured max enrollments of the fabric-ca-server, use max_enrollments; \nIf > configured max enrollments of the fabric-ca-server, error." }, - "name": { + "affiliation": { "type": "string", "description": "The affiliation path of the new identity.\n" }, @@ -1858,7 +1905,7 @@ }, "required": [ "id", - "name", + "affiliation", "attrs" ] } @@ -1899,7 +1946,7 @@ ], "description": "The maximum number of times that the secret can be used to enroll. \nIf 0, use the configured max_enrollments of the fabric-ca-server; \nIf > 0 and <= configured max enrollments of the fabric-ca-server, use max_enrollments; \nIf > configured max enrollments of the fabric-ca-server, error." }, - "name": { + "affiliation": { "type": "string", "description": "The affiliation path of the new identity.\n" }, @@ -2044,7 +2091,7 @@ ], "description": "The maximum number of times that the secret can be used to enroll. \nIf 0, use the configured max_enrollments of the fabric-ca-server; \nIf > 0 and <= configured max enrollments of the fabric-ca-server, use max_enrollments; \nIf > configured max enrollments of the fabric-ca-server, error." }, - "name": { + "affiliation": { "type": "string", "description": "The affiliation path of the new identity.\n" }, @@ -2184,7 +2231,7 @@ ], "description": "The new maximum number of times that the secret can be used to enroll. \nIf -1, use the configured max_enrollments of the fabric-ca-server; \nIf > 0 and <= configured max enrollments of the fabric-ca-server, use max_enrollments; \nIf > configured max enrollments of the fabric-ca-server, error." }, - "name": { + "affiliation": { "type": "string", "description": "The affiliation path of the identity.\n" }, @@ -2261,9 +2308,9 @@ ], "description": "The maximum number of times that the secret can be used to enroll. \nIf 0, use the configured max_enrollments of the fabric-ca-server; \nIf > 0 and <= configured max enrollments of the fabric-ca-server, use max_enrollments; \nIf > configured max enrollments of the fabric-ca-server, error." }, - "name": { + "affiliation": { "type": "string", - "description": "The affiliation path of the new identity.\n" + "description": "The affiliation path of the identity.\n" }, "attrs": { "type": "array", @@ -2389,6 +2436,10 @@ "type": "object", "properties": { "Success": { + "type": "boolean", + "description": "Boolean indicating if the request was successful." + }, + "Result": { "description": "The identity that was deleted.", "type": "object", "properties": { @@ -2400,13 +2451,6 @@ "type": "string", "description": "The type of the identity (e.g. *user*, *app*, *peer*, *orderer*, etc)" }, - "secret": { - "type": [ - "string", - "null" - ], - "description": "The enrollment secret. If not provided, a random secret is generated." - }, "max_enrollments": { "type": [ "integer", @@ -2414,9 +2458,9 @@ ], "description": "The maximum number of times that the secret can be used to enroll. \nIf 0, use the configured max_enrollments of the fabric-ca-server; \nIf > 0 and <= configured max enrollments of the fabric-ca-server, use max_enrollments; \nIf > configured max enrollments of the fabric-ca-server, error." }, - "name": { + "affiliation": { "type": "string", - "description": "The affiliation path of the new identity.\n" + "description": "The affiliation path of the identity.\n" }, "attrs": { "type": "array", @@ -2450,7 +2494,7 @@ }, "required": [ "id", - "name", + "affiliation", "attrs" ] },