-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implemented path config rotate
Signed-off-by: manhtukhang <[email protected]>
- Loading branch information
1 parent
e1f843e
commit 7778568
Showing
5 changed files
with
264 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
package nxr | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/hashicorp/vault/sdk/framework" | ||
"github.com/hashicorp/vault/sdk/logical" | ||
gopw "github.com/sethvargo/go-password/password" | ||
) | ||
|
||
const ( | ||
configRotatePath = "config/rotate" | ||
) | ||
|
||
// pathConfigRotate replaces the configurated admin's password | ||
// with a random one. | ||
func pathConfigRotate(b *backend) *framework.Path { | ||
return &framework.Path{ | ||
Pattern: configRotatePath, | ||
// TODO: rotate by creating a new user with same privileges and revoking the current one | ||
// Fields: map[string]*framework.FieldSchema{ | ||
// "username": { | ||
// Type: framework.TypeLowerCaseString, | ||
// Description: "Optional. Overwrite the username to access Nexus Repository API", | ||
// DisplayAttrs: &framework.DisplayAttributes{ | ||
// Name: "Username", | ||
// Sensitive: false, | ||
// }, | ||
// }, | ||
// }, | ||
Operations: map[logical.Operation]framework.OperationHandler{ | ||
logical.UpdateOperation: &framework.PathOperation{ | ||
Callback: b.pathConfigRotateWrite, | ||
Summary: "Rotate the Nexus Repository admin credential", | ||
}, | ||
}, | ||
HelpSynopsis: pathConfigRotateHelpSynopsis, | ||
HelpDescription: pathConfigRotateHelpDescription, | ||
} | ||
} | ||
|
||
func (b *backend) pathConfigRotateWrite(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||
config, err := b.fetchAdminConfig(ctx, req.Storage) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if config == nil { | ||
return logical.ErrorResponse("admin configuration not found"), nil | ||
} | ||
|
||
newPw, err := gopw.Generate(64, 10, 10, false, false) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
nxrClient, err := b.getClient(ctx, req.Storage) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if err = nxrClient.Security.User.ChangePassword(config.Username, newPw); err != nil { | ||
return nil, err | ||
} | ||
|
||
// TODO: check if new password is usable (assume to yes) | ||
|
||
config.Password = newPw | ||
|
||
entry, err := logical.StorageEntryJSON(configAdminPath, config) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if err := req.Storage.Put(ctx, entry); err != nil { | ||
return nil, err | ||
} | ||
|
||
// reset the client so the next invocation will pick up the new configuration | ||
b.reset() | ||
|
||
return nil, nil | ||
} | ||
|
||
const ( | ||
pathConfigRotateHelpSynopsis = `Rotate the Nexus Repository admin credential.` | ||
|
||
pathConfigRotateHelpDescription = ` | ||
This will rotate the "username" and "password" used to access Nexus Repository from this plugin. | ||
A new user is created first then revokes the old one.` | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
package nxr | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"testing" | ||
|
||
"github.com/hashicorp/vault/sdk/logical" | ||
"github.com/stretchr/testify/assert" | ||
|
||
"go.nhat.io/httpmock" | ||
) | ||
|
||
const ( | ||
userChangePasswordURI = "/service/rest/v1/security/users/%s/change-password" | ||
) | ||
|
||
func TestConfigRotateWithMockApi(t *testing.T) { | ||
b, reqStorage := getTestBackend(t) | ||
|
||
t.Run("Unhappy cases", func(t *testing.T) { | ||
// Rotate empty config | ||
err := testConfigRotateWrite(b, reqStorage) | ||
assert.Error(t, err) | ||
|
||
// GatewayTimeout | ||
srv := httpmock.New(func(s *httpmock.Server) { | ||
s.ExpectPut(fmt.Sprintf(userChangePasswordURI, testConfigAdminUsername)). | ||
ReturnCode(httpmock.StatusGatewayTimeout) | ||
})(t) | ||
|
||
_, err = writeConfigAdmin(b, reqStorage, map[string]interface{}{ | ||
"username": testConfigAdminUsername, | ||
"password": testConfigAdminPassword, | ||
"url": srv.URL(), | ||
}) | ||
assert.NoError(t, err) | ||
|
||
err = testConfigRotateWrite(b, reqStorage) | ||
assert.Error(t, err) | ||
|
||
// User does not have permission to perform the operation | ||
srv = httpmock.New(func(s *httpmock.Server) { | ||
s.ExpectPut(fmt.Sprintf(userChangePasswordURI, testConfigAdminUsername)). | ||
ReturnCode(httpmock.StatusForbidden) | ||
})(t) | ||
|
||
_, err = writeConfigAdmin(b, reqStorage, map[string]interface{}{ | ||
"username": testConfigAdminUsername, | ||
"password": testConfigAdminPassword, | ||
"url": srv.URL(), | ||
}) | ||
assert.NoError(t, err) | ||
|
||
err = testConfigRotateWrite(b, reqStorage) | ||
assert.Error(t, err) | ||
}) | ||
|
||
t.Run("Happy cases", func(t *testing.T) { | ||
srv := httpmock.New(func(s *httpmock.Server) { | ||
s.ExpectPut(fmt.Sprintf(userChangePasswordURI, testConfigAdminUsername)). | ||
ReturnCode(httpmock.StatusOK) | ||
})(t) | ||
|
||
_, err := writeConfigAdmin(b, reqStorage, map[string]interface{}{ | ||
"username": testConfigAdminUsername, | ||
"password": testConfigAdminPassword, | ||
"url": srv.URL(), | ||
}) | ||
assert.NoError(t, err) | ||
|
||
err = testConfigRotateWrite(b, reqStorage) | ||
assert.NoError(t, err) | ||
}) | ||
} | ||
|
||
func testConfigRotateWrite(b logical.Backend, s logical.Storage) error { | ||
resp, err := b.HandleRequest(context.Background(), &logical.Request{ | ||
Operation: logical.UpdateOperation, | ||
Path: configRotatePath, | ||
Storage: s, | ||
}) | ||
if err != nil { | ||
return err | ||
} | ||
if resp != nil && resp.IsError() { | ||
return resp.Error() | ||
} | ||
return nil | ||
} |