diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 76cca1f..0f4a58f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -42,10 +42,10 @@ jobs: env: BATON_LOG_LEVEL: debug # Change these to the correct IDs for your test data - CONNECTOR_GRANT: '' - CONNECTOR_ENTITLEMENT: '' - CONNECTOR_PRINCIPAL: '' - CONNECTOR_PRINCIPAL_TYPE: '' + CONNECTOR_GRANT: 'vault:tnturck9vro:write:user:IDjSP9BVbJ3RnPr2FonGxXp5' + CONNECTOR_ENTITLEMENT: 'vault:tnturck9vro:write' + CONNECTOR_PRINCIPAL_TYPE: 'user' + CONNECTOR_PRINCIPAL: 'IDjSP9BVbJ3RnPr2FonGxXp5' VGS_VAULT: 'tnturck9vro' VGS_ORGANIZATION_ID: 'ACeVfPS9pFLe5izbhMo4rxMV' steps: @@ -66,25 +66,25 @@ jobs: - name: Check for grant before revoking if: env.CONNECTOR_ENTITLEMENT != '' && env.CONNECTOR_PRINCIPAL != '' run: | - ./baton-vgs --vault ${{ env.VGS_VAULT }} + ./baton-vgs --vault ${{ env.VGS_VAULT }} --service-account-client-id ${{ secrets.BATON_SERVICE_ACCOUNT_CLIENT_ID }} --service-account-client-secret ${{ secrets.BATON_SERVICE_ACCOUNT_CLIENT_SECRET }} --organization-id ${{ env.VGS_ORGANIZATION_ID }} baton grants --entitlement ${{ env.CONNECTOR_ENTITLEMENT }} --output-format=json | jq -e ".grants | any(.principal.id.resource ==\"${{ env.CONNECTOR_PRINCIPAL }}\")" - name: Revoke grants if: env.CONNECTOR_GRANT != '' run: | - ./baton-vgs --vault ${{ env.VGS_VAULT }} - ./baton-vgs --vault ${{ env.VGS_VAULT }} --revoke-grant ${{ env.CONNECTOR_GRANT }} + ./baton-vgs --vault ${{ env.VGS_VAULT }} --service-account-client-id ${{ secrets.BATON_SERVICE_ACCOUNT_CLIENT_ID }} --service-account-client-secret ${{ secrets.BATON_SERVICE_ACCOUNT_CLIENT_SECRET }} --organization-id ${{ env.VGS_ORGANIZATION_ID }} + ./baton-vgs --vault ${{ env.VGS_VAULT }} --service-account-client-id ${{ secrets.BATON_SERVICE_ACCOUNT_CLIENT_ID }} --service-account-client-secret ${{ secrets.BATON_SERVICE_ACCOUNT_CLIENT_SECRET }} --organization-id ${{ env.VGS_ORGANIZATION_ID }} --revoke-grant ${{ env.CONNECTOR_GRANT }} - name: Check grant was revoked if: env.CONNECTOR_ENTITLEMENT != '' && env.CONNECTOR_PRINCIPAL != '' run: | - ./baton-vgs --vault ${{ env.VGS_VAULT }} + ./baton-vgs --vault ${{ env.VGS_VAULT }} --service-account-client-id ${{ secrets.BATON_SERVICE_ACCOUNT_CLIENT_ID }} --service-account-client-secret ${{ secrets.BATON_SERVICE_ACCOUNT_CLIENT_SECRET }} --organization-id ${{ env.VGS_ORGANIZATION_ID }} baton grants --entitlement ${{ env.CONNECTOR_ENTITLEMENT }} --output-format=json | jq -e ".grants | any(.principal.id.resource !=\"${{ env.CONNECTOR_PRINCIPAL }}\")" - name: Grant entitlement if: env.CONNECTOR_ENTITLEMENT != '' && env.CONNECTOR_PRINCIPAL != '' && env.CONNECTOR_PRINCIPAL_TYPE != '' run: | - ./baton-vgs --vault ${{ env.VGS_VAULT }} - ./baton-vgs --vault ${{ env.VGS_VAULT }} --grant-entitlement ${{ env.CONNECTOR_ENTITLEMENT }} --grant-principal ${{ env.CONNECTOR_PRINCIPAL }} --grant-principal-type ${{ env.CONNECTOR_PRINCIPAL_TYPE }} + ./baton-vgs --vault ${{ env.VGS_VAULT }} --service-account-client-id ${{ secrets.BATON_SERVICE_ACCOUNT_CLIENT_ID }} --service-account-client-secret ${{ secrets.BATON_SERVICE_ACCOUNT_CLIENT_SECRET }} --organization-id ${{ env.VGS_ORGANIZATION_ID }} + ./baton-vgs --vault ${{ env.VGS_VAULT }} --service-account-client-id ${{ secrets.BATON_SERVICE_ACCOUNT_CLIENT_ID }} --service-account-client-secret ${{ secrets.BATON_SERVICE_ACCOUNT_CLIENT_SECRET }} --organization-id ${{ env.VGS_ORGANIZATION_ID }} --grant-entitlement ${{ env.CONNECTOR_ENTITLEMENT }} --grant-principal ${{ env.CONNECTOR_PRINCIPAL }} --grant-principal-type ${{ env.CONNECTOR_PRINCIPAL_TYPE }} - name: Check grant was re-granted if: env.CONNECTOR_ENTITLEMENT != '' && env.CONNECTOR_PRINCIPAL != '' run: | - ./baton-vgs --vault ${{ env.VGS_VAULT }} + ./baton-vgs --vault ${{ env.VGS_VAULT }} --service-account-client-id ${{ secrets.BATON_SERVICE_ACCOUNT_CLIENT_ID }} --service-account-client-secret ${{ secrets.BATON_SERVICE_ACCOUNT_CLIENT_SECRET }} --organization-id ${{ env.VGS_ORGANIZATION_ID }} baton grants --entitlement ${{ env.CONNECTOR_ENTITLEMENT }} --output-format=json | jq -e ".grants | any(.principal.id.resource ==\"${{ env.CONNECTOR_PRINCIPAL }}\")" \ No newline at end of file diff --git a/pkg/client/client.go b/pkg/client/client.go index 619f1e8..a0c4bfa 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -203,6 +203,9 @@ func (v *VGSClient) ListOrganizations(ctx context.Context) ([]Organization, erro return organizations, nil } +// ListUsers +// Read all organizations users. Retrieves list of all users linked to an organization. NOTE: This endpoint does not return pending invitations. +// https://www.verygoodsecurity.com/docs/accounts/api/#tag/users/paths/~1organizations~1{organizationId}~1members/get func (v *VGSClient) ListUsers(ctx context.Context, orgId, vaultId string) ([]OrganizationUser, error) { var ( users []OrganizationUser @@ -251,6 +254,9 @@ func (v *VGSClient) ListUsers(ctx context.Context, orgId, vaultId string) ([]Org return users, nil } +// ListUserInvites +// Get user invitations to an organization. Returns list of user invitations to an organization. +// https://www.verygoodsecurity.com/docs/accounts/api/#tag/invites/paths/~1organizations~1{organizationId}~1invites/get func (v *VGSClient) ListUserInvites(ctx context.Context, orgId string) ([]OrganizationUser, error) { var ( userInvites []OrganizationUser @@ -300,6 +306,9 @@ func (v *VGSClient) ListUserInvites(ctx context.Context, orgId string) ([]Organi return userInvites, nil } +// ListVaultUsers +// Read all vault users. Retrieves list of all users linked to a vault. +// https://www.verygoodsecurity.com/docs/accounts/api/#tag/users/paths/~1vaults~1{vaultIdentifier}~1members/get func (v *VGSClient) ListVaultUsers(ctx context.Context, vaultId string) ([]vaultUserAPI, error) { var vaultUsersAPIData vaultUsersAPIData if !strings.Contains(v.token.Scope, "organization-users:read") { @@ -379,7 +388,10 @@ func (v *VGSClient) ListVaults(ctx context.Context) ([]Vault, error) { return organizationVaults, nil } -func (v *VGSClient) UpdateVault(ctx context.Context, vaultIdentifier, userId, role string) error { +// UpdateVault +// Update user access to vault. Requires organization-users:write scope. +// https://www.verygoodsecurity.com/docs/accounts/api/#tag/users/paths/~1vaults~1{vaultIdentifier}~1members~1{userId}/put +func (v *VGSClient) UpdateUserAccessVault(ctx context.Context, vaultIdentifier, userId, role string) error { var ( body Body payload = []byte(fmt.Sprintf(`{"data":{"attributes":{"role":"%s"}}}`, role)) @@ -427,3 +439,46 @@ func (v *VGSClient) UpdateVault(ctx context.Context, vaultIdentifier, userId, ro return nil } + +// RevokeUserAccessVault +// Revoke user access to vault. Requires organization-users:write scope. +// https://www.verygoodsecurity.com/docs/accounts/api/#tag/users/paths/~1vaults~1{vaultIdentifier}~1members~1{userId}/delete +func (v *VGSClient) RevokeUserAccessVault(ctx context.Context, vaultIdentifier, userId string) error { + if !strings.Contains(v.token.Scope, "organization-users:write") { + return fmt.Errorf("organization-users:write scope not found") + } + + strUrl, err := url.JoinPath(v.serviceEndpoint, "vaults", vaultIdentifier, "members", userId) + if err != nil { + return err + } + + uri, err := url.Parse(strUrl) + if err != nil { + return err + } + + req, err := v.httpClient.NewRequest(ctx, + http.MethodDelete, + uri, + WithAcceptVndJSONHeader(), + WithContentTypeVndHeader(), + WithAuthorizationBearerHeader(v.GetToken()), + ) + + if err != nil { + return err + } + + resp, err := v.httpClient.Do(req) + if err != nil { + return err + } + + defer resp.Body.Close() + if resp.StatusCode != http.StatusNoContent { + return errors.New("user details not updated") + } + + return nil +} diff --git a/pkg/connector/vault.go b/pkg/connector/vault.go index f150d39..8a632ca 100644 --- a/pkg/connector/vault.go +++ b/pkg/connector/vault.go @@ -128,7 +128,7 @@ func (v *vaultResourceType) Grant(ctx context.Context, principal *v2.Resource, e } role = parts[len(parts)-1] - err = v.client.UpdateVault(ctx, + err = v.client.UpdateUserAccessVault(ctx, entitlement.Resource.Id.Resource, principal.Id.Resource, role) @@ -145,6 +145,36 @@ func (v *vaultResourceType) Grant(ctx context.Context, principal *v2.Resource, e } func (v *vaultResourceType) Revoke(ctx context.Context, grant *v2.Grant) (annotations.Annotations, error) { + l := ctxzap.Extract(ctx) + principal := grant.Principal + entitlement := grant.Entitlement + if principal.Id.ResourceType != resourceTypeUser.Id { + l.Warn( + "baton-vgs: only users can be revoked role membership", + zap.String("principal_type", principal.Id.ResourceType), + zap.String("principal_id", principal.Id.Resource), + ) + return nil, fmt.Errorf("baton-vgs: only users can be revoked role membership") + } + + _, _, err := parseEntitlementID(entitlement.Id) + if err != nil { + return nil, err + } + + err = v.client.RevokeUserAccessVault(ctx, + entitlement.Resource.Id.Resource, + principal.Id.Resource, + ) + if err != nil { + return nil, err + } + + l.Warn("Role Membership has been removed.", + zap.String("vaultIdentifier", entitlement.Resource.Id.Resource), + zap.String("userId", principal.Id.Resource), + ) + return nil, nil }