-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds the <mountPath>/token/<role> endpoint to return an Oauth access token. This access token is not leased because these tokens have a TTL of 60m and are not revokable upstream. Caveats: - The <mountPath>/roles/<role> backend will create a separate App/SP with the same logic as the <mountPath>/roles/<role> creds. So, a unified App/Service Principal is not used between the various endpoints for a given role. - No changes were made to how deleting a role revokes the cloud resources used by the <mountPath>/creds/<role> endpoint. - An "existing Service Principal" still creates an App password as opposed to a service principal password.
- Loading branch information
1 parent
6ac0282
commit b479907
Showing
13 changed files
with
633 additions
and
114 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package api | ||
|
||
import ( | ||
"github.com/Azure/go-autorest/autorest/adal" | ||
"github.com/Azure/go-autorest/autorest/azure/auth" | ||
) | ||
|
||
type TokenClient interface { | ||
GetToken(c auth.ClientCredentialsConfig) (adal.Token, error) | ||
} | ||
|
||
var _ TokenClient = (*AccessTokenClient)(nil) | ||
|
||
type AccessTokenClient struct{} | ||
|
||
// GetToken fetches a new Azure OAuth2 bearer token from the given clients | ||
// credentials and tenant. | ||
func (p *AccessTokenClient) GetToken(c auth.ClientCredentialsConfig) (adal.Token, error) { | ||
t, err := c.ServicePrincipalToken() | ||
if err != nil { | ||
return adal.Token{}, err | ||
} | ||
|
||
err = t.Refresh() | ||
if err != nil { | ||
return adal.Token{}, err | ||
} | ||
|
||
return t.Token(), nil | ||
} |
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,126 @@ | ||
package azuresecrets | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"strings" | ||
|
||
azureadal "github.com/Azure/go-autorest/autorest/adal" | ||
azureauth "github.com/Azure/go-autorest/autorest/azure/auth" | ||
"github.com/fatih/structs" | ||
"github.com/hashicorp/vault/sdk/framework" | ||
"github.com/hashicorp/vault/sdk/logical" | ||
) | ||
|
||
const ( | ||
azureAppNotFoundErrCode = 700016 | ||
) | ||
|
||
func pathAccessToken(b *azureSecretBackend) *framework.Path { | ||
return &framework.Path{ | ||
Pattern: fmt.Sprintf("token/%s", framework.GenericNameRegex("role")), | ||
Fields: map[string]*framework.FieldSchema{ | ||
"role": { | ||
Type: framework.TypeLowerCaseString, | ||
Description: "Name of the Vault role", | ||
}, | ||
"resource": { | ||
Type: framework.TypeString, | ||
Description: "The specific Azure audience of a generated access token", | ||
Default: "https://management.azure.com/", | ||
}, | ||
}, | ||
Operations: map[logical.Operation]framework.OperationHandler{ | ||
logical.ReadOperation: &framework.PathOperation{ | ||
Callback: b.pathAccessTokenRead, | ||
}, | ||
}, | ||
HelpSynopsis: pathAccessTokenHelpSyn, | ||
HelpDescription: pathAccessTokenHelpDesc, | ||
} | ||
} | ||
|
||
func (b *azureSecretBackend) pathAccessTokenRead(ctx context.Context, request *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||
roleName := data.Get("role").(string) | ||
resource := data.Get("resource").(string) | ||
|
||
role, err := getRole(ctx, roleName, request.Storage) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if role == nil { | ||
return logical.ErrorResponse("role '%s' does not exist", roleName), nil | ||
} | ||
|
||
if role.CredentialType != credentialTypeSP { | ||
return logical.ErrorResponse("role '%s' cannot generate access tokens (has secret type %s)", roleName, role.CredentialType), nil | ||
} | ||
|
||
if role.Credentials == nil { | ||
return logical.ErrorResponse("role '%s' configured before plugin supported access tokens (update or recreate role)", roleName), nil | ||
} | ||
|
||
return b.secretAccessTokenResponse(ctx, request.Storage, role, resource) | ||
} | ||
|
||
func (b *azureSecretBackend) secretAccessTokenResponse(ctx context.Context, storage logical.Storage, role *roleEntry, resource string) (*logical.Response, error) { | ||
client, err := b.getClient(ctx, storage) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
cc := azureauth.NewClientCredentialsConfig(role.ApplicationID, role.Credentials.Password, client.settings.TenantID) | ||
cc.Resource = resource | ||
token, err := b.getToken(ctx, client, cc) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// access_tokens are not revocable therefore do not return a framework.Secret (i.e. a lease) | ||
return &logical.Response{Data: structsMap(token)}, nil | ||
} | ||
|
||
func structsMap(s interface{}) map[string]interface{} { | ||
t := structs.New(s) | ||
t.TagName = "json" | ||
return t.Map() | ||
} | ||
|
||
func (b *azureSecretBackend) getToken(ctx context.Context, client *client, c azureauth.ClientCredentialsConfig) (azureadal.Token, error) { | ||
token, err := retry(ctx, func() (interface{}, bool, error) { | ||
t, err := client.provider.GetToken(c) | ||
|
||
if hasAzureErrorCode(err, azureAppNotFoundErrCode) { | ||
return nil, false, nil | ||
} else if err != nil { | ||
return nil, true, err | ||
} | ||
|
||
return t, true, nil | ||
}) | ||
|
||
var t azureadal.Token | ||
if token != nil { | ||
t = token.(azureadal.Token) | ||
} | ||
|
||
return t, err | ||
} | ||
|
||
func hasAzureErrorCode(e error, code int) bool { | ||
tErr, ok := e.(azureadal.TokenRefreshError) | ||
|
||
// use a pattern match as TokenRefreshError is not easily parsable | ||
return ok && tErr != nil && strings.Contains(tErr.Error(), fmt.Sprint(code)) | ||
} | ||
|
||
const pathAccessTokenHelpSyn = ` | ||
Request an access token for a given Vault role. | ||
` | ||
|
||
const pathAccessTokenHelpDesc = ` | ||
This path creates access token credentials. The associated role must | ||
be created ahead of time with either an existing App/Service Principal or | ||
else a dynamic Service Principal will be created. | ||
` |
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,105 @@ | ||
package azuresecrets | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
|
||
"github.com/hashicorp/vault/sdk/logical" | ||
) | ||
|
||
func Test_azureSecretBackend_pathAccessTokenRead(t *testing.T) { | ||
b, s := getTestBackend(t, true) | ||
|
||
t.Run("token generated", func(t *testing.T) { | ||
role := generateUUID() | ||
testRoleCreate(t, b, s, role, testStaticSPRole) | ||
|
||
resp, err := b.HandleRequest(context.Background(), &logical.Request{ | ||
Operation: logical.ReadOperation, | ||
Path: "token/" + role, | ||
Storage: s, | ||
}) | ||
|
||
assertErrorIsNil(t, err) | ||
|
||
if resp.IsError() { | ||
t.Fatalf("receive response error: %v", resp.Error()) | ||
} | ||
|
||
if _, ok := resp.Data["access_token"]; !ok { | ||
t.Fatalf("access_token not found in response") | ||
} | ||
|
||
if _, ok := resp.Data["refresh_token"]; !ok { | ||
t.Fatalf("refresh_token not found in response") | ||
} | ||
|
||
if _, ok := resp.Data["expires_in"]; !ok { | ||
t.Fatalf("expires_in not found in response") | ||
} | ||
|
||
if _, ok := resp.Data["expires_on"]; !ok { | ||
t.Fatalf("expires_on not found in response") | ||
} | ||
|
||
if _, ok := resp.Data["not_before"]; !ok { | ||
t.Fatalf("not_before not found in response") | ||
} | ||
|
||
r, ok := resp.Data["resource"] | ||
if !ok { | ||
t.Fatalf("resource not found in response") | ||
} | ||
if r != "https://management.azure.com/" { | ||
t.Fatalf("resource not equal to requested") | ||
} | ||
|
||
if _, ok := resp.Data["token_type"]; !ok { | ||
t.Fatalf("token_type not found in response") | ||
} | ||
}) | ||
|
||
t.Run("non default resource token generated", func(t *testing.T) { | ||
role := generateUUID() | ||
testRoleCreate(t, b, s, role, testStaticSPRole) | ||
|
||
resource := "https://resource.endpoint/" | ||
resp, err := b.HandleRequest(context.Background(), &logical.Request{ | ||
Operation: logical.ReadOperation, | ||
Path: "token/" + role, | ||
Data: map[string]interface{}{ | ||
"resource": resource, | ||
}, | ||
Storage: s, | ||
}) | ||
|
||
assertErrorIsNil(t, err) | ||
|
||
if resp.IsError() { | ||
t.Fatalf("receive response error: %v", resp.Error()) | ||
} | ||
|
||
r, ok := resp.Data["resource"] | ||
if !ok { | ||
t.Fatalf("resource not found in response") | ||
} | ||
if r != resource { | ||
t.Fatalf("resource not equal to requested") | ||
} | ||
}) | ||
|
||
t.Run("role does not exist", func(t *testing.T) { | ||
role := generateUUID() | ||
resp, err := b.HandleRequest(context.Background(), &logical.Request{ | ||
Operation: logical.ReadOperation, | ||
Path: "token/" + role, | ||
Storage: s, | ||
}) | ||
|
||
assertErrorIsNil(t, err) | ||
|
||
if !resp.IsError() { | ||
t.Fatal("expected missing role error") | ||
} | ||
}) | ||
} |
Oops, something went wrong.