Merge branch 'master' into jo-ie11-fixes

This commit is contained in:
Joshua Ogle 2018-04-17 14:07:05 -06:00 committed by GitHub
commit 7bb9490403
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 1165 additions and 288 deletions

View File

@ -1,3 +1,20 @@
## 0.10.1 (Unreleased)
IMPROVEMENTS:
* identity: Add the ability to disable an entity. Disabling an entity does not
revoke associated tokens, but while the entity is disabled they cannot be
used. [GH-4353]
* auth/token: Add to the token lookup response, the policies inherited due to
identity associations [GH-4366]
BUG FIXES:
* identity: Persist entity memberships in external identity groups across
mounts [GH-4365]
* secret/gcp: Fix panic on rollback when a roleset wasn't created properly
[GH-4344]
## 0.10.0 (April 10th, 2018) ## 0.10.0 (April 10th, 2018)
SECURITY: SECURITY:

View File

@ -606,7 +606,7 @@ CLUSTER_SYNTHESIS_COMPLETE:
// Force https as we'll always be TLS-secured // Force https as we'll always be TLS-secured
u, err := url.ParseRequestURI(coreConfig.ClusterAddr) u, err := url.ParseRequestURI(coreConfig.ClusterAddr)
if err != nil { if err != nil {
c.UI.Error(fmt.Sprintf("Error parsing cluster address %s: %v", coreConfig.RedirectAddr, err)) c.UI.Error(fmt.Sprintf("Error parsing cluster address %s: %v", coreConfig.ClusterAddr, err))
return 11 return 11
} }
u.Scheme = "https" u.Scheme = "https"

View File

@ -200,6 +200,9 @@ type Entity struct {
// the entities belonging to a particular bucket during invalidation of the // the entities belonging to a particular bucket during invalidation of the
// storage key. // storage key.
BucketKeyHash string `sentinel:"" protobuf:"bytes,9,opt,name=bucket_key_hash,json=bucketKeyHash" json:"bucket_key_hash,omitempty"` BucketKeyHash string `sentinel:"" protobuf:"bytes,9,opt,name=bucket_key_hash,json=bucketKeyHash" json:"bucket_key_hash,omitempty"`
// Disabled indicates whether tokens associated with the account should not
// be able to be used
Disabled bool `sentinel:"" protobuf:"varint,11,opt,name=disabled" json:"disabled,omitempty"`
} }
func (m *Entity) Reset() { *m = Entity{} } func (m *Entity) Reset() { *m = Entity{} }
@ -270,6 +273,13 @@ func (m *Entity) GetBucketKeyHash() string {
return "" return ""
} }
func (m *Entity) GetDisabled() bool {
if m != nil {
return m.Disabled
}
return false
}
// Alias represents the alias that gets stored inside of the // Alias represents the alias that gets stored inside of the
// entity object in storage and also represents in an in-memory index of an // entity object in storage and also represents in an in-memory index of an
// alias object. // alias object.
@ -392,43 +402,44 @@ func init() {
func init() { proto.RegisterFile("types.proto", fileDescriptor0) } func init() { proto.RegisterFile("types.proto", fileDescriptor0) }
var fileDescriptor0 = []byte{ var fileDescriptor0 = []byte{
// 603 bytes of a gzipped FileDescriptorProto // 617 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x93, 0xdd, 0x6e, 0xd3, 0x30, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x94, 0xdd, 0x6e, 0xd3, 0x30,
0x14, 0xc7, 0xd5, 0xa6, 0x9f, 0x27, 0x5d, 0x37, 0x2c, 0x84, 0x4c, 0xa5, 0x41, 0x37, 0x69, 0x28, 0x14, 0xc7, 0xd5, 0xa6, 0x1f, 0xe9, 0x69, 0xd7, 0x0d, 0x0b, 0x21, 0x53, 0x69, 0xd0, 0x4d, 0x1a,
0x70, 0x91, 0x49, 0xe3, 0x86, 0x8d, 0x0b, 0x34, 0xc1, 0x80, 0x09, 0x21, 0xa1, 0x68, 0x5c, 0x47, 0x2a, 0x5c, 0x64, 0xd2, 0xb8, 0x61, 0xe3, 0x02, 0x4d, 0x30, 0x60, 0x42, 0x48, 0x28, 0x1a, 0xd7,
0x6e, 0xe2, 0xb5, 0xd6, 0x92, 0x38, 0x8a, 0x1d, 0x44, 0x5e, 0x87, 0x97, 0xe1, 0x69, 0x78, 0x07, 0x91, 0x1b, 0x7b, 0xad, 0xb5, 0x24, 0x8e, 0x62, 0x17, 0x91, 0xd7, 0xe1, 0xd5, 0xb8, 0xe6, 0x1d,
0xe4, 0xe3, 0xa6, 0x0d, 0x74, 0x7c, 0x4c, 0xdb, 0x9d, 0xf3, 0x3f, 0xc7, 0xc7, 0x27, 0xe7, 0xff, 0x90, 0x8f, 0x9b, 0x36, 0xd0, 0xf1, 0x31, 0x6d, 0x77, 0xf6, 0xff, 0x1c, 0x1f, 0x1f, 0x9f, 0xff,
0x3b, 0xe0, 0xea, 0x2a, 0xe7, 0xca, 0xcf, 0x0b, 0xa9, 0x25, 0x19, 0x88, 0x98, 0x67, 0x5a, 0xe8, 0x2f, 0x81, 0xbe, 0x29, 0x73, 0xa1, 0x83, 0xbc, 0x50, 0x46, 0x11, 0x5f, 0x72, 0x91, 0x19, 0x69,
0x6a, 0xf2, 0x78, 0x2e, 0xe5, 0x3c, 0xe1, 0x87, 0xa8, 0xcf, 0xca, 0xcb, 0x43, 0x2d, 0x52, 0xae, 0xca, 0xd1, 0xe3, 0x99, 0x52, 0xb3, 0x44, 0x1c, 0xa2, 0x3e, 0x5d, 0x5c, 0x1e, 0x1a, 0x99, 0x0a,
0x34, 0x4b, 0x73, 0x9b, 0xba, 0xff, 0xad, 0x03, 0xdd, 0x77, 0x85, 0x2c, 0x73, 0x32, 0x86, 0xb6, 0x6d, 0x58, 0x9a, 0xbb, 0xd4, 0xfd, 0x6f, 0x2d, 0x68, 0xbf, 0x2b, 0xd4, 0x22, 0x27, 0x43, 0x68,
0x88, 0x69, 0x6b, 0xda, 0xf2, 0x86, 0x41, 0x5b, 0xc4, 0x84, 0x40, 0x27, 0x63, 0x29, 0xa7, 0x6d, 0x4a, 0x4e, 0x1b, 0xe3, 0xc6, 0xa4, 0x17, 0x36, 0x25, 0x27, 0x04, 0x5a, 0x19, 0x4b, 0x05, 0x6d,
0x54, 0xf0, 0x4c, 0x26, 0x30, 0xc8, 0x65, 0x22, 0x22, 0xc1, 0x15, 0x75, 0xa6, 0x8e, 0x37, 0x0c, 0xa2, 0x82, 0x6b, 0x32, 0x02, 0x3f, 0x57, 0x89, 0x8c, 0xa5, 0xd0, 0xd4, 0x1b, 0x7b, 0x93, 0x5e,
0x56, 0xdf, 0xc4, 0x83, 0x9d, 0x9c, 0x15, 0x3c, 0xd3, 0xe1, 0xdc, 0xd4, 0x0b, 0x45, 0xac, 0x68, 0xb8, 0xda, 0x93, 0x09, 0xec, 0xe4, 0xac, 0x10, 0x99, 0x89, 0x66, 0xb6, 0x5e, 0x24, 0xb9, 0xa6,
0x07, 0x73, 0xc6, 0x56, 0xc7, 0x67, 0xce, 0x63, 0x45, 0x9e, 0xc1, 0xbd, 0x94, 0xa7, 0x33, 0x5e, 0x2d, 0xcc, 0x19, 0x3a, 0x1d, 0xaf, 0x39, 0xe7, 0x9a, 0x3c, 0x83, 0x7b, 0xa9, 0x48, 0xa7, 0xa2,
0x84, 0xb6, 0x4b, 0x4c, 0xed, 0x62, 0xea, 0xb6, 0x0d, 0x9c, 0xa1, 0x6e, 0x72, 0x8f, 0x61, 0x90, 0x88, 0x5c, 0x97, 0x98, 0xda, 0xc6, 0xd4, 0x6d, 0x17, 0x38, 0x43, 0xdd, 0xe6, 0x1e, 0x83, 0x9f,
0x72, 0xcd, 0x62, 0xa6, 0x19, 0xed, 0x4d, 0x1d, 0xcf, 0x3d, 0xda, 0xf5, 0xeb, 0xbf, 0xf3, 0xb1, 0x0a, 0xc3, 0x38, 0x33, 0x8c, 0x76, 0xc6, 0xde, 0xa4, 0x7f, 0xb4, 0x1b, 0x54, 0xaf, 0x0b, 0xb0,
0xa2, 0xff, 0x71, 0x19, 0x3f, 0xcb, 0x74, 0x51, 0x05, 0xab, 0x74, 0xf2, 0x0a, 0xb6, 0xa2, 0x82, 0x62, 0xf0, 0x71, 0x19, 0x3f, 0xcb, 0x4c, 0x51, 0x86, 0xab, 0x74, 0xf2, 0x0a, 0xb6, 0xe2, 0x42,
0x33, 0x2d, 0x64, 0x16, 0x9a, 0xdf, 0xa6, 0xfd, 0x69, 0xcb, 0x73, 0x8f, 0x26, 0xbe, 0x9d, 0x89, 0x30, 0x23, 0x55, 0x16, 0xd9, 0x67, 0xd3, 0xee, 0xb8, 0x31, 0xe9, 0x1f, 0x8d, 0x02, 0x37, 0x93,
0x5f, 0xcf, 0xc4, 0xbf, 0xa8, 0x67, 0x12, 0x8c, 0xea, 0x0b, 0x46, 0x22, 0x6f, 0x60, 0x27, 0x61, 0xa0, 0x9a, 0x49, 0x70, 0x51, 0xcd, 0x24, 0x1c, 0x54, 0x07, 0xac, 0x44, 0xde, 0xc0, 0x4e, 0xc2,
0x4a, 0x87, 0x65, 0x1e, 0x33, 0xcd, 0x6d, 0x8d, 0xc1, 0x3f, 0x6b, 0x8c, 0xcd, 0x9d, 0xcf, 0x78, 0xb4, 0x89, 0x16, 0x39, 0x67, 0x46, 0xb8, 0x1a, 0xfe, 0x3f, 0x6b, 0x0c, 0xed, 0x99, 0xcf, 0x78,
0x05, 0xab, 0xec, 0xc1, 0x28, 0x95, 0xb1, 0xb8, 0xac, 0x42, 0x91, 0xc5, 0xfc, 0x2b, 0x1d, 0x4e, 0x04, 0xab, 0xec, 0xc1, 0x20, 0x55, 0x5c, 0x5e, 0x96, 0x91, 0xcc, 0xb8, 0xf8, 0x4a, 0x7b, 0xe3,
0x5b, 0x5e, 0x27, 0x70, 0xad, 0x76, 0x6e, 0x24, 0xf2, 0x04, 0xb6, 0x67, 0x65, 0x74, 0xc5, 0x75, 0xc6, 0xa4, 0x15, 0xf6, 0x9d, 0x76, 0x6e, 0x25, 0xf2, 0x04, 0xb6, 0xa7, 0x8b, 0xf8, 0x4a, 0x98,
0x78, 0xc5, 0xab, 0x70, 0xc1, 0xd4, 0x82, 0x02, 0x4e, 0x7d, 0xcb, 0xca, 0x1f, 0x78, 0xf5, 0x9e, 0xe8, 0x4a, 0x94, 0xd1, 0x9c, 0xe9, 0x39, 0x05, 0x9c, 0xfa, 0x96, 0x93, 0x3f, 0x88, 0xf2, 0x3d,
0xa9, 0x05, 0x39, 0x80, 0x2e, 0x4b, 0x04, 0x53, 0xd4, 0xc5, 0x2e, 0xb6, 0xd7, 0x93, 0x38, 0x35, 0xd3, 0x73, 0x72, 0x00, 0x6d, 0x96, 0x48, 0xa6, 0x69, 0x1f, 0xbb, 0xd8, 0x5e, 0x4f, 0xe2, 0xd4,
0x72, 0x60, 0xa3, 0xc6, 0x39, 0x43, 0x03, 0x1d, 0x59, 0xe7, 0xcc, 0x79, 0xf2, 0x12, 0xb6, 0x7e, 0xca, 0xa1, 0x8b, 0x5a, 0xe7, 0x2c, 0x0d, 0x74, 0xe0, 0x9c, 0xb3, 0xeb, 0xd1, 0x4b, 0xd8, 0xfa,
0x99, 0x13, 0xd9, 0x01, 0xe7, 0x8a, 0x57, 0x4b, 0xbf, 0xcd, 0x91, 0xdc, 0x87, 0xee, 0x17, 0x96, 0x65, 0x4e, 0x64, 0x07, 0xbc, 0x2b, 0x51, 0x2e, 0xfd, 0xb6, 0x4b, 0x72, 0x1f, 0xda, 0x5f, 0x58,
0x94, 0xb5, 0xe3, 0xf6, 0xe3, 0xa4, 0xfd, 0xa2, 0xb5, 0xff, 0xdd, 0x81, 0x9e, 0xb5, 0x84, 0x3c, 0xb2, 0xa8, 0x1c, 0x77, 0x9b, 0x93, 0xe6, 0x8b, 0xc6, 0xfe, 0x77, 0x0f, 0x3a, 0xce, 0x12, 0xf2,
0x85, 0x3e, 0x3e, 0xc2, 0x15, 0x6d, 0xa1, 0x1d, 0x1b, 0x4d, 0xd4, 0xf1, 0x25, 0x50, 0xed, 0x0d, 0x14, 0xba, 0x78, 0x89, 0xd0, 0xb4, 0x81, 0x76, 0x6c, 0x34, 0x51, 0xc5, 0x97, 0x40, 0x35, 0x37,
0xa0, 0x9c, 0x06, 0x50, 0x27, 0x0d, 0x7b, 0x3b, 0x58, 0xef, 0xd1, 0xba, 0x9e, 0x7d, 0xf2, 0xff, 0x80, 0xf2, 0x6a, 0x40, 0x9d, 0xd4, 0xec, 0x6d, 0x61, 0xbd, 0x47, 0xeb, 0x7a, 0xee, 0xca, 0xff,
0xfd, 0xed, 0xde, 0x81, 0xbf, 0xbd, 0x1b, 0xfb, 0x8b, 0x34, 0x17, 0x73, 0x1e, 0x37, 0x69, 0xee, 0xf7, 0xb7, 0x7d, 0x07, 0xfe, 0x76, 0x6e, 0xec, 0x2f, 0xd2, 0x5c, 0xcc, 0x04, 0xaf, 0xd3, 0xdc,
0xd7, 0x34, 0x9b, 0xc0, 0x9a, 0xe6, 0xe6, 0xfe, 0x0c, 0x7e, 0xdb, 0x9f, 0x6b, 0x20, 0x18, 0x5e, 0xad, 0x68, 0xb6, 0x81, 0x35, 0xcd, 0xf5, 0xef, 0xc7, 0xff, 0xed, 0xfb, 0xb9, 0x06, 0x82, 0xde,
0x03, 0xc1, 0xed, 0x9c, 0xfc, 0xe1, 0x40, 0x17, 0x6d, 0xda, 0x58, 0xf7, 0x3d, 0x18, 0x45, 0x2c, 0x75, 0x10, 0x8c, 0xc0, 0xe7, 0x52, 0xb3, 0x69, 0x22, 0x38, 0x72, 0xe0, 0x87, 0xab, 0xfd, 0xed,
0x93, 0x99, 0x88, 0x58, 0x12, 0xae, 0x7c, 0x73, 0x57, 0xda, 0x79, 0x4c, 0x76, 0x01, 0x52, 0x59, 0x5c, 0xfe, 0xe1, 0x41, 0x1b, 0x2d, 0xdc, 0xf8, 0x15, 0xec, 0xc1, 0x20, 0x66, 0x99, 0xca, 0x64,
0x66, 0x3a, 0x44, 0xba, 0xac, 0x8d, 0x43, 0x54, 0x2e, 0xaa, 0x9c, 0x93, 0x03, 0x18, 0xdb, 0x30, 0xcc, 0x92, 0x68, 0xe5, 0x69, 0x7f, 0xa5, 0x9d, 0x73, 0xb2, 0x0b, 0x90, 0xaa, 0x45, 0x66, 0x22,
0x8b, 0x22, 0xae, 0x94, 0x2c, 0x68, 0xc7, 0xf6, 0x8f, 0xea, 0xe9, 0x52, 0x5c, 0x57, 0xc9, 0x99, 0x24, 0xcf, 0x59, 0xdc, 0x43, 0xe5, 0xa2, 0xcc, 0x05, 0x39, 0x80, 0xa1, 0x0b, 0xb3, 0x38, 0x16,
0x5e, 0xa0, 0x67, 0x75, 0x95, 0x4f, 0x4c, 0x2f, 0xfe, 0xbe, 0xf0, 0xd8, 0xfa, 0x1f, 0x81, 0xa8, 0x5a, 0xab, 0x82, 0xb6, 0xdc, 0xdb, 0x50, 0x3d, 0x5d, 0x8a, 0xeb, 0x2a, 0x39, 0x33, 0x73, 0xf4,
0x01, 0xeb, 0x37, 0x00, 0xdb, 0x80, 0x64, 0x70, 0x07, 0x90, 0x0c, 0x6f, 0x0c, 0xc9, 0x31, 0x3c, 0xb3, 0xaa, 0xf2, 0x89, 0x99, 0xf9, 0xdf, 0x7f, 0x06, 0xd8, 0xfa, 0x1f, 0x61, 0xa9, 0xe0, 0xeb,
0x5c, 0x42, 0x72, 0x59, 0xc8, 0x34, 0x6c, 0x4e, 0x5a, 0x51, 0x40, 0x12, 0x1e, 0xd8, 0x84, 0xb7, 0xd6, 0xe0, 0xdb, 0x00, 0xc8, 0xbf, 0x03, 0x80, 0x7a, 0x37, 0x06, 0xe8, 0x18, 0x1e, 0x2e, 0x01,
0x85, 0x4c, 0x5f, 0xaf, 0x87, 0xae, 0x6e, 0xe5, 0xf7, 0xac, 0x87, 0xbd, 0x3d, 0xff, 0x19, 0x00, 0xba, 0x2c, 0x54, 0x1a, 0xd5, 0x27, 0xad, 0x29, 0x20, 0x25, 0x0f, 0x5c, 0xc2, 0xdb, 0x42, 0xa5,
0x00, 0xff, 0xff, 0x8e, 0x4a, 0xc5, 0xdb, 0x1f, 0x06, 0x00, 0x00, 0xaf, 0xd7, 0x43, 0xd7, 0xb7, 0xf2, 0x7b, 0xda, 0xc1, 0xde, 0x9e, 0xff, 0x0c, 0x00, 0x00, 0xff,
0xff, 0x60, 0x0b, 0xc9, 0x74, 0x3b, 0x06, 0x00, 0x00,
} }

View File

@ -110,6 +110,10 @@ message Entity {
// MFASecrets holds the MFA secrets indexed by the identifier of the MFA // MFASecrets holds the MFA secrets indexed by the identifier of the MFA
// method configuration. // method configuration.
//map<string, mfa.Secret> mfa_secrets = 10; //map<string, mfa.Secret> mfa_secrets = 10;
// Disabled indicates whether tokens associated with the account should not
// be able to be used
bool disabled = 11;
} }
// Alias represents the alias that gets stored inside of the // Alias represents the alias that gets stored inside of the

View File

@ -273,6 +273,10 @@ var (
// ErrPermissionDenied is returned if the client is not authorized // ErrPermissionDenied is returned if the client is not authorized
ErrPermissionDenied = errors.New("permission denied") ErrPermissionDenied = errors.New("permission denied")
// ErrDisabledEntity is returned if the entity tied to a token is marked as
// disabled
ErrEntityDisabled = errors.New("entity associated with token is disabled")
// ErrMultiAuthzPending is returned if the the request needs more // ErrMultiAuthzPending is returned if the the request needs more
// authorizations // authorizations
ErrMultiAuthzPending = errors.New("request needs further approval") ErrMultiAuthzPending = errors.New("request needs further approval")

View File

@ -12,17 +12,37 @@ export default Ember.Component.extend({
return `${this.get('filename')}-${new Date().toISOString()}.${this.get('extension')}`; return `${this.get('filename')}-${new Date().toISOString()}.${this.get('extension')}`;
}), }),
href: computed('data', 'mime', 'stringify', function() { fileLike: computed('data', 'mime', 'strigify', 'download', function() {
let file;
let data = this.get('data'); let data = this.get('data');
const mime = this.get('mime'); let filename = this.get('download');
let mime = this.get('mime');
if (this.get('stringify')) { if (this.get('stringify')) {
data = JSON.stringify(data, null, 2); data = JSON.stringify(data, null, 2);
} }
if (window.navigator.msSaveOrOpenBlob) {
const file = new File([data], { type: mime }); file = new Blob([data], { type: mime });
return window.URL.createObjectURL(file); file.name = filename;
} else {
file = new File([data], filename, { type: mime });
}
return file;
}), }),
href: computed('fileLike', function() {
return window.URL.createObjectURL(this.get('fileLike'));
}),
click(event) {
if (!window.navigator.msSaveOrOpenBlob) {
return;
}
event.preventDefault();
let file = this.get('fileLike');
//lol whyyyy
window.navigator.msSaveOrOpenBlob(file, file.name);
},
actionText: 'Download', actionText: 'Download',
data: null, data: null,
filename: null, filename: null,

View File

@ -8,13 +8,13 @@ const { computed } = Ember;
export default Ember.Controller.extend({ export default Ember.Controller.extend({
mountTypes: [ mountTypes: [
{ label: 'AWS', value: 'aws' }, { label: 'AWS', value: 'aws' },
{ label: 'Cassandra', value: 'cassandra' }, { label: 'Cassandra', value: 'cassandra', deprecated: true },
{ label: 'Consul', value: 'consul' }, { label: 'Consul', value: 'consul' },
{ label: 'Databases', value: 'database' }, { label: 'Databases', value: 'database' },
{ label: 'Google Cloud', value: 'gcp' }, { label: 'Google Cloud', value: 'gcp' },
{ label: 'KV', value: 'kv' }, { label: 'KV', value: 'kv' },
{ label: 'MongoDB', value: 'mongodb' }, { label: 'MongoDB', value: 'mongodb', deprecated: true },
{ label: 'MS SQL', value: 'mssql', deprecated: true }, { label: 'MSSQL', value: 'mssql', deprecated: true },
{ label: 'MySQL', value: 'mysql', deprecated: true }, { label: 'MySQL', value: 'mysql', deprecated: true },
{ label: 'Nomad', value: 'nomad' }, { label: 'Nomad', value: 'nomad' },
{ label: 'PKI', value: 'pki' }, { label: 'PKI', value: 'pki' },

View File

@ -46,7 +46,7 @@ export default EditBase.extend({
const parentKey = params.secret ? params.secret : ''; const parentKey = params.secret ? params.secret : '';
return Ember.RSVP.hash({ return Ember.RSVP.hash({
secret: this.createModel(transition, parentKey), secret: this.createModel(transition, parentKey),
capabilities: this.capabilities(parentKey), capabilities: {},
}); });
}, },
}); });

View File

@ -27,12 +27,6 @@ export default Ember.Route.extend({
} }
}, },
capabilities(secret) {
const { backend } = this.paramsFor('vault.cluster.secrets.backend');
const path = backend + '/' + secret;
return this.store.findRecord('capabilities', path);
},
getModelType(backend, tab) { getModelType(backend, tab) {
const types = { const types = {
transit: 'transit-key', transit: 'transit-key',
@ -49,29 +43,26 @@ export default Ember.Route.extend({
const secret = params.secret ? params.secret : ''; const secret = params.secret ? params.secret : '';
const { backend } = this.paramsFor('vault.cluster.secrets.backend'); const { backend } = this.paramsFor('vault.cluster.secrets.backend');
const backends = this.modelFor('vault.cluster.secrets').mapBy('id'); const backends = this.modelFor('vault.cluster.secrets').mapBy('id');
return Ember.RSVP.hash({ return this.store
secrets: this.store .lazyPaginatedQuery(this.getModelType(backend, params.tab), {
.lazyPaginatedQuery(this.getModelType(backend, params.tab), { id: secret,
id: secret, backend,
backend, responsePath: 'data.keys',
responsePath: 'data.keys', page: params.page,
page: params.page, pageFilter: params.pageFilter,
pageFilter: params.pageFilter, size: 100,
size: 100, })
}) .then(model => {
.then(model => { this.set('has404', false);
this.set('has404', false); return model;
return model; })
}) .catch(err => {
.catch(err => { if (backends.includes(backend) && err.httpStatus === 404 && secret === '') {
if (backends.includes(backend) && err.httpStatus === 404 && secret === '') { return [];
return []; } else {
} else { throw err;
throw err; }
} });
}),
capabilities: this.capabilities(secret),
});
}, },
afterModel(model) { afterModel(model) {
@ -107,8 +98,7 @@ export default Ember.Route.extend({
const has404 = this.get('has404'); const has404 = this.get('has404');
controller.set('hasModel', true); controller.set('hasModel', true);
controller.setProperties({ controller.setProperties({
model: model.secrets, model,
capabilities: model.capabilities,
baseKey: { id: secret }, baseKey: { id: secret },
has404, has404,
backend, backend,
@ -125,7 +115,7 @@ export default Ember.Route.extend({
} }
controller.setProperties({ controller.setProperties({
filter: filter || '', filter: filter || '',
page: model.secrets.get('meta.currentPage') || 1, page: model.get('meta.currentPage') || 1,
}); });
} }
}, },

View File

@ -5,11 +5,16 @@ import UnloadModelRoute from 'vault/mixins/unload-model-route';
export default Ember.Route.extend(UnloadModelRoute, { export default Ember.Route.extend(UnloadModelRoute, {
capabilities(secret) { capabilities(secret) {
const { backend } = this.paramsFor('vault.cluster.secrets.backend'); const { backend } = this.paramsFor('vault.cluster.secrets.backend');
let backendModel = this.store.peekRecord('secret-engine', backend);
let backendType = backendModel.get('type');
let version = backendModel.get('options.version');
let path; let path;
if (backend === 'transit') { if (backendType === 'transit') {
path = backend + '/keys/' + secret; path = backend + '/keys/' + secret;
} else if (backend === 'ssh' || backend === 'aws') { } else if (backendType === 'ssh' || backendType === 'aws') {
path = backend + '/roles/' + secret; path = backend + '/roles/' + secret;
} else if (version && version === 2) {
path = backend + '/data/' + secret;
} else { } else {
path = backend + '/' + secret; path = backend + '/' + secret;
} }

View File

@ -52,20 +52,18 @@
</div> </div>
<div class="field is-grouped-split box is-fullwidth is-bottomless"> <div class="field is-grouped-split box is-fullwidth is-bottomless">
<div class="control"> <div class="control">
{{#if capabilities.canCreate}} <button
<button type="submit"
type="submit" disabled={{buttonDisabled}}
disabled={{buttonDisabled}} class="button is-primary"
class="button is-primary" data-test-role-aws-create=true
data-test-role-aws-create=true >
> {{#if (eq mode 'create')}}
{{#if (eq mode 'create')}} Create role
Create role {{else if (eq mode 'edit')}}
{{else if (eq mode 'edit')}} Save
Save {{/if}}
{{/if}} </button>
</button>
{{/if}}
{{#secret-link {{#secret-link
mode=(if (eq mode "create") "list" "show") mode=(if (eq mode "create") "list" "show")
class="button" class="button"

View File

@ -5,20 +5,18 @@
</div> </div>
<div class="field is-grouped-split box is-fullwidth is-bottomless"> <div class="field is-grouped-split box is-fullwidth is-bottomless">
<div class="control"> <div class="control">
{{#if capabilities.canCreate}} <button
<button type="submit"
type="submit" disabled={{buttonDisabled}}
disabled={{buttonDisabled}} class="button is-primary"
class="button is-primary" data-test-role-create=true
data-test-role-create=true >
> {{#if (eq mode 'create')}}
{{#if (eq mode 'create')}} Create role
Create role {{else if (eq mode 'edit')}}
{{else if (eq mode 'edit')}} Save
Save {{/if}}
{{/if}} </button>
</button>
{{/if}}
{{#secret-link {{#secret-link
mode=(if (eq mode "create") "list" "show") mode=(if (eq mode "create") "list" "show")
class="button" class="button"

View File

@ -25,20 +25,18 @@
</div> </div>
<div class="field is-grouped-split box is-fullwidth is-bottomless"> <div class="field is-grouped-split box is-fullwidth is-bottomless">
<div class="control"> <div class="control">
{{#if capabilities.canCreate}} <button
<button type="submit"
type="submit" disabled={{buttonDisabled}}
disabled={{buttonDisabled}} class="button is-primary"
class="button is-primary" data-test-role-ssh-create=true
data-test-role-ssh-create=true >
> {{#if (eq mode 'create')}}
{{#if (eq mode 'create')}} Create role
Create role {{else if (eq mode 'edit')}}
{{else if (eq mode 'edit')}} Save
Save {{/if}}
{{/if}} </button>
</button>
{{/if}}
{{#secret-link {{#secret-link
mode=(if (eq mode "create") "list" "show") mode=(if (eq mode "create") "list" "show")
class="button" class="button"

View File

@ -34,16 +34,14 @@
<div class="field is-grouped box is-fullwidth is-bottomless"> <div class="field is-grouped box is-fullwidth is-bottomless">
<div class="control"> <div class="control">
{{#if capabilities.canCreate}} <button
<button type="submit"
type="submit" disabled={{buttonDisabled}}
disabled={{buttonDisabled}} class="button is-primary"
class="button is-primary" data-test-secret-save=true
data-test-secret-save=true >
>
Save Save
</button> </button>
{{/if}}
</div> </div>
<div class="control"> <div class="control">
{{#secret-link {{#secret-link

View File

@ -80,11 +80,11 @@
<div class="field"> <div class="field">
<div class="b-checkbox"> <div class="b-checkbox">
<input type="checkbox" <input type="checkbox"
data-test-transit-key-convergent-encryption
id="key-convergent" id="key-convergent"
class="styled" class="styled"
checked={{key.convergentEncryption}} checked={{key.convergentEncryption}}
onchange={{action "convergentEncryptionChange" value="target.checked"}} onchange={{action "convergentEncryptionChange" value="target.checked"}}
data-test-transit-key-convergent-encryption=true
/> />
<label for="key-convergent" class="is-label"> <label for="key-convergent" class="is-label">
Enable convergent encryption Enable convergent encryption
@ -94,18 +94,16 @@
{{/if}} {{/if}}
</div> </div>
<div class="field is-grouped box is-fullwidth is-bottomless"> <div class="field is-grouped box is-fullwidth is-bottomless">
{{#if capabilities.canCreate}} <div class="control">
<div class="control"> <button
<button data-test-transit-key-create
type="submit" type="submit"
disabled={{requestInFlight}} disabled={{requestInFlight}}
class="button is-primary {{if requestInFlight 'is-loading'}}" class="button is-primary {{if requestInFlight 'is-loading'}}"
data-test-transit-key-create=true >
> Create encryption key
Create encryption key </button>
</button> </div>
</div>
{{/if}}
<div class="control"> <div class="control">
{{#secret-link {{#secret-link
mode="list" mode="list"

View File

@ -20,7 +20,7 @@
</h1> </h1>
</div> </div>
<div class="level-right field is-grouped"> <div class="level-right field is-grouped">
{{#if (and (not-eq tab "certs") capabilities.canCreate)}} {{#if (not-eq tab "certs")}}
<div class="control"> <div class="control">
{{#secret-link {{#secret-link
mode="create" mode="create"
@ -114,8 +114,7 @@
{{else}} {{else}}
<div class="box is-sideless"> <div class="box is-sideless">
{{#if filterFocused}} {{#if filterFocused}}
There are no {{pluralize options.item}} matching <code>{{filter}}</code> There are no {{pluralize options.item}} matching <code>{{filter}}</code>, press <kbd>ENTER</kbd> to add one.
{{#if capabilities.canCreate}}, press <kbd>ENTER</kbd> to add one{{/if}}.
{{else}} {{else}}
There are no {{pluralize options.item}} matching <code>{{filter}}</code>. There are no {{pluralize options.item}} matching <code>{{filter}}</code>.
{{/if}} {{/if}}

View File

@ -28,7 +28,7 @@
<div class="level-left"> <div class="level-left">
<div> <div>
<a data-test-secret-path href={{href-to 'vault.cluster.secrets.backend.list-root' backend.id}} class="has-text-black has-text-weight-semibold"> <a data-test-secret-path href={{href-to 'vault.cluster.secrets.backend.list-root' backend.id}} class="has-text-black has-text-weight-semibold">
{{i-con glyph="folder" size=14 class="has-text-grey-light"}} {{backend.path}} {{i-con glyph="folder" size=14 class="has-text-grey-light"}}{{backend.path}}
</a> </a>
<br /> <br />
<span class="tag"> <span class="tag">

View File

@ -56,10 +56,10 @@
<div class="control is-expanded"> <div class="control is-expanded">
<div class="select is-fullwidth"> <div class="select is-fullwidth">
<select <select
data-test-secret-backend-version
id="backend-type" id="backend-type"
value={{selectedType}} value={{selectedType}}
onchange={{action (mut version) value="target.value"}} onchange={{action (mut version) value="target.value"}}
data-test-secret-backend-type=true
> >
{{#each (array 1 2) as |versionOption|}} {{#each (array 1 2) as |versionOption|}}
<option selected={{eq version versionOption}} value={{versionOption}}> <option selected={{eq version versionOption}} value={{versionOption}}>

View File

@ -5,8 +5,10 @@ if(process.argv[2]){
process.exit(0); process.exit(0);
} }
process.env.TERM = 'dumb';
var fs = require('fs'); var fs = require('fs');
var path = require('path'); var path = require('path');
var readline = require('readline')
var spawn = require('child_process').spawn; var spawn = require('child_process').spawn;
var vault = spawn( var vault = spawn(
'vault', 'vault',
@ -21,21 +23,15 @@ var vault = spawn(
] ]
); );
// https://github.com/chalk/ansi-regex/blob/master/index.js
var ansiPattern = [
'[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\\u0007)',
'(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))'
].join('|');
var ANSI_REGEX = new RegExp(ansiPattern, 'g');
var output = ''; var output = '';
var unseal, root; var unseal, root;
vault.stdout.on('data', function(data) {
var stringData = data.toString().replace(ANSI_REGEX, '');
output = output + stringData;
console.log(stringData);
readline.createInterface({
input : vault.stdout,
terminal : false
}).on('line', function(line) {
output = output + line;
console.log(line);
var unsealMatch = output.match(/Unseal Key\: (.+)$/m); var unsealMatch = output.match(/Unseal Key\: (.+)$/m);
if (unsealMatch && !unseal) { unseal = unsealMatch[1] }; if (unsealMatch && !unseal) { unseal = unsealMatch[1] };
var rootMatch = output.match(/Root Token\: (.+)$/m); var rootMatch = output.match(/Root Token\: (.+)$/m);

View File

@ -1,5 +1,8 @@
import { test } from 'qunit'; import { test, skip } from 'qunit';
import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance'; import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
import secretList from 'vault/tests/pages/secrets/backend/list';
import secretEdit from 'vault/tests/pages/secrets/backend/kv/edit-secret';
import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend';
import Ember from 'ember'; import Ember from 'ember';
let adapterException; let adapterException;
@ -9,7 +12,11 @@ moduleForAcceptance('Acceptance | leases', {
beforeEach() { beforeEach() {
adapterException = Ember.Test.adapter.exception; adapterException = Ember.Test.adapter.exception;
Ember.Test.adapter.exception = () => null; Ember.Test.adapter.exception = () => null;
return authLogin();
authLogin();
this.enginePath = `kv-for-lease-${new Date().getTime()}`;
// need a version 1 mount for leased secrets here
return mountSecrets.visit().path(this.enginePath).type('kv').version(1).submit();
}, },
afterEach() { afterEach() {
Ember.Test.adapter.exception = adapterException; Ember.Test.adapter.exception = adapterException;
@ -18,43 +25,31 @@ moduleForAcceptance('Acceptance | leases', {
}); });
const createSecret = (context, isRenewable) => { const createSecret = (context, isRenewable) => {
const now = new Date().getTime(); context.name = `secret-${new Date().getTime()}`;
const secretContents = { secret: 'foo' }; secretList.visitRoot({ backend: context.enginePath });
secretList.create();
if (isRenewable) { if (isRenewable) {
secretContents.ttl = '30h'; secretEdit.createSecret(context.name, 'ttl', '30h');
} else {
secretEdit.createSecret(context.name, 'foo', 'bar');
} }
context.secret = {
name: isRenewable ? `renew-secret-${now}` : `secret-${now}`,
text: JSON.stringify(secretContents),
};
//create a secret so we have a lease (server is running in -dev-leased-kv mode)
visit('/vault/secrets/secret/list');
click('[data-test-secret-create]');
fillIn('[data-test-secret-path]', context.secret.name);
andThen(() => {
const codeMirror = find('.CodeMirror');
// UI keeps state so once we flip to json, we don't need to again
if (!codeMirror.length) {
click('[data-test-secret-json-toggle]');
}
});
andThen(() => {
find('.CodeMirror').get(0).CodeMirror.setValue(context.secret.text);
});
click('[data-test-secret-save]');
}; };
const navToDetail = secret => { const navToDetail = context => {
visit('/vault/access/leases/'); visit('/vault/access/leases/');
click('[data-test-lease-link="secret/"]'); // all the
click(`[data-test-lease-link="secret/${secret.name}/"]`); click(`[data-test-lease-link="${context.enginePath}/"]`);
// way down
click(`[data-test-lease-link="${context.enginePath}/data/"]`);
// the tree
click(`[data-test-lease-link="${context.enginePath}/data/${context.name}/"]`);
click(`[data-test-lease-link]:eq(0)`); click(`[data-test-lease-link]:eq(0)`);
}; };
test('it renders the show page', function(assert) { test('it renders the show page', function(assert) {
createSecret(this); createSecret(this);
navToDetail(this.secret); navToDetail(this);
andThen(() => { return andThen(() => {
assert.equal( assert.equal(
currentRouteName(), currentRouteName(),
'vault.cluster.access.leases.show', 'vault.cluster.access.leases.show',
@ -68,9 +63,10 @@ test('it renders the show page', function(assert) {
}); });
}); });
test('it renders the show page with a picker', function(assert) { // skip for now until we find an easy way to generate a renewable lease
skip('it renders the show page with a picker', function(assert) {
createSecret(this, true); createSecret(this, true);
navToDetail(this.secret); navToDetail(this);
andThen(() => { andThen(() => {
assert.equal( assert.equal(
currentRouteName(), currentRouteName(),
@ -83,7 +79,7 @@ test('it renders the show page with a picker', function(assert) {
test('it removes leases upon revocation', function(assert) { test('it removes leases upon revocation', function(assert) {
createSecret(this); createSecret(this);
navToDetail(this.secret); navToDetail(this);
click('[data-test-lease-revoke] button'); click('[data-test-lease-revoke] button');
click('[data-test-confirm-button]'); click('[data-test-confirm-button]');
andThen(() => { andThen(() => {
@ -93,10 +89,11 @@ test('it removes leases upon revocation', function(assert) {
'it navigates back to the leases root on revocation' 'it navigates back to the leases root on revocation'
); );
}); });
click('[data-test-lease-link="secret/"]'); click(`[data-test-lease-link="${this.enginePath}/"]`);
click('[data-test-lease-link="data/"]');
andThen(() => { andThen(() => {
assert.equal( assert.equal(
find(`[data-test-lease-link="secret/${this.secret.name}/"]`).length, find(`[data-test-lease-link="${this.enginePath}/data/${this.name}/"]`).length,
0, 0,
'link to the lease was removed with revocation' 'link to the lease was removed with revocation'
); );
@ -105,7 +102,7 @@ test('it removes leases upon revocation', function(assert) {
test('it removes branches when a prefix is revoked', function(assert) { test('it removes branches when a prefix is revoked', function(assert) {
createSecret(this); createSecret(this);
visit('/vault/access/leases/list/secret/'); visit(`/vault/access/leases/list/${this.enginePath}`);
click('[data-test-lease-revoke-prefix] button'); click('[data-test-lease-revoke-prefix] button');
click('[data-test-confirm-button]'); click('[data-test-confirm-button]');
andThen(() => { andThen(() => {
@ -115,7 +112,7 @@ test('it removes branches when a prefix is revoked', function(assert) {
'it navigates back to the leases root on revocation' 'it navigates back to the leases root on revocation'
); );
assert.equal( assert.equal(
find('[data-test-lease-link="secret/"]').length, find(`[data-test-lease-link="${this.enginePath}/"]`).length,
0, 0,
'link to the prefix was removed with revocation' 'link to the prefix was removed with revocation'
); );

View File

@ -4,14 +4,26 @@ import editPage from 'vault/tests/pages/secrets/backend/kv/edit-secret';
import showPage from 'vault/tests/pages/secrets/backend/kv/show'; import showPage from 'vault/tests/pages/secrets/backend/kv/show';
import listPage from 'vault/tests/pages/secrets/backend/list'; import listPage from 'vault/tests/pages/secrets/backend/list';
import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend';
import Pretender from 'pretender';
moduleForAcceptance('Acceptance | secrets/secret/create', { moduleForAcceptance('Acceptance | secrets/secret/create', {
beforeEach() { beforeEach() {
this.server = new Pretender(function() {
this.post('/v1/**', this.passthrough);
this.put('/v1/**', this.passthrough);
this.get('/v1/**', this.passthrough);
this.delete('/v1/**', this.passthrough);
});
return authLogin(); return authLogin();
}, },
afterEach() {
this.server.shutdown();
},
}); });
test('it creates a secret and redirects', function(assert) { test('it creates a secret and redirects', function(assert) {
const path = `kv-${new Date().getTime()}`; const path = `kv-path-${new Date().getTime()}`;
listPage.visitRoot({ backend: 'secret' }); listPage.visitRoot({ backend: 'secret' });
andThen(() => { andThen(() => {
assert.equal(currentRouteName(), 'vault.cluster.secrets.backend.list-root', 'navigates to the list page'); assert.equal(currentRouteName(), 'vault.cluster.secrets.backend.list-root', 'navigates to the list page');
@ -20,7 +32,30 @@ test('it creates a secret and redirects', function(assert) {
listPage.create(); listPage.create();
editPage.createSecret(path, 'foo', 'bar'); editPage.createSecret(path, 'foo', 'bar');
andThen(() => { andThen(() => {
let capabilitiesReq = this.server.passthroughRequests.findBy('url', '/v1/sys/capabilities-self');
assert.equal(
JSON.parse(capabilitiesReq.requestBody).path,
`secret/data/${path}`,
'calls capabilites with the correct path'
);
assert.equal(currentRouteName(), 'vault.cluster.secrets.backend.show', 'redirects to the show page'); assert.equal(currentRouteName(), 'vault.cluster.secrets.backend.show', 'redirects to the show page');
assert.ok(showPage.editIsPresent, 'shows the edit button'); assert.ok(showPage.editIsPresent, 'shows the edit button');
}); });
}); });
test('version 1 performs the correct capabilities lookup', function(assert) {
let enginePath = `kv-${new Date().getTime()}`;
let secretPath = 'foo/bar';
mountSecrets.visit().path(enginePath).type('kv').version(1).submit();
listPage.create();
editPage.createSecret(secretPath, 'foo', 'bar');
andThen(() => {
let capabilitiesReq = this.server.passthroughRequests.findBy('url', '/v1/sys/capabilities-self');
assert.equal(
JSON.parse(capabilitiesReq.requestBody).path,
`${enginePath}/${secretPath}`,
'calls capabilites with the correct path'
);
});
});

View File

@ -6,6 +6,7 @@ export default create({
path: fillable('[data-test-secret-backend-path]'), path: fillable('[data-test-secret-backend-path]'),
submit: clickable('[data-test-secret-backend-submit]'), submit: clickable('[data-test-secret-backend-submit]'),
toggleOptions: clickable('[data-test-secret-backend-options]'), toggleOptions: clickable('[data-test-secret-backend-options]'),
version: fillable('[data-test-secret-backend-version]'),
maxTTLVal: fillable('[data-test-ttl-value]', { scope: '[data-test-secret-backend-max-ttl]' }), maxTTLVal: fillable('[data-test-ttl-value]', { scope: '[data-test-secret-backend-max-ttl]' }),
maxTTLUnit: fillable('[data-test-ttl-unit]', { scope: '[data-test-secret-backend-max-ttl]' }), maxTTLUnit: fillable('[data-test-ttl-unit]', { scope: '[data-test-secret-backend-max-ttl]' }),
defaultTTLVal: fillable('[data-test-ttl-value]', { scope: '[data-test-secret-backend-default-ttl]' }), defaultTTLVal: fillable('[data-test-ttl-value]', { scope: '[data-test-secret-backend-default-ttl]' }),

View File

@ -802,6 +802,10 @@ func (c *Core) checkToken(ctx context.Context, req *logical.Request, unauth bool
} }
} }
if entity != nil && entity.Disabled {
return nil, te, logical.ErrEntityDisabled
}
// Check if this is a root protected path // Check if this is a root protected path
rootPath := c.router.RootPath(req.Path) rootPath := c.router.RootPath(req.Path)
@ -1319,20 +1323,21 @@ func (c *Core) sealInitCommon(ctx context.Context, req *logical.Request) (retErr
return retErr return retErr
} }
// Since there is no token store in standby nodes, sealing cannot be done.
// Ideally, the request has to be forwarded to leader node for validation
// and the operation should be performed. But for now, just returning with
// an error and recommending a vault restart, which essentially does the
// same thing.
if c.standby {
c.logger.Error("vault cannot seal when in standby mode; please restart instead")
retErr = multierror.Append(retErr, errors.New("vault cannot seal when in standby mode; please restart instead"))
c.stateLock.RUnlock()
return retErr
}
// Validate the token is a root token // Validate the token is a root token
acl, te, entity, err := c.fetchACLTokenEntryAndEntity(req.ClientToken) acl, te, entity, err := c.fetchACLTokenEntryAndEntity(req.ClientToken)
if err != nil { if err != nil {
// Since there is no token store in standby nodes, sealing cannot
// be done. Ideally, the request has to be forwarded to leader node
// for validation and the operation should be performed. But for now,
// just returning with an error and recommending a vault restart, which
// essentially does the same thing.
if c.standby {
c.logger.Error("vault cannot seal when in standby mode; please restart instead")
retErr = multierror.Append(retErr, errors.New("vault cannot seal when in standby mode; please restart instead"))
c.stateLock.RUnlock()
return retErr
}
retErr = multierror.Append(retErr, err) retErr = multierror.Append(retErr, err)
c.stateLock.RUnlock() c.stateLock.RUnlock()
return retErr return retErr
@ -1341,10 +1346,12 @@ func (c *Core) sealInitCommon(ctx context.Context, req *logical.Request) (retErr
// Audit-log the request before going any further // Audit-log the request before going any further
auth := &logical.Auth{ auth := &logical.Auth{
ClientToken: req.ClientToken, ClientToken: req.ClientToken,
Policies: te.Policies, }
Metadata: te.Meta, if te != nil {
DisplayName: te.DisplayName, auth.Policies = te.Policies
EntityID: te.EntityID, auth.Metadata = te.Meta
auth.DisplayName = te.DisplayName
auth.EntityID = te.EntityID
} }
logInput := &audit.LogInput{ logInput := &audit.LogInput{
@ -1358,6 +1365,12 @@ func (c *Core) sealInitCommon(ctx context.Context, req *logical.Request) (retErr
return retErr return retErr
} }
if entity != nil && entity.Disabled {
retErr = multierror.Append(retErr, logical.ErrEntityDisabled)
c.stateLock.RUnlock()
return retErr
}
// Attempt to use the token (decrement num_uses) // Attempt to use the token (decrement num_uses)
// On error bail out; if the token has been revoked, bail out too // On error bail out; if the token has been revoked, bail out too
if te != nil { if te != nil {
@ -1450,10 +1463,12 @@ func (c *Core) StepDown(req *logical.Request) (retErr error) {
// Audit-log the request before going any further // Audit-log the request before going any further
auth := &logical.Auth{ auth := &logical.Auth{
ClientToken: req.ClientToken, ClientToken: req.ClientToken,
Policies: te.Policies, }
Metadata: te.Meta, if te != nil {
DisplayName: te.DisplayName, auth.Policies = te.Policies
EntityID: te.EntityID, auth.Metadata = te.Meta
auth.DisplayName = te.DisplayName
auth.EntityID = te.EntityID
} }
logInput := &audit.LogInput{ logInput := &audit.LogInput{
@ -1466,6 +1481,12 @@ func (c *Core) StepDown(req *logical.Request) (retErr error) {
return retErr return retErr
} }
if entity != nil && entity.Disabled {
retErr = multierror.Append(retErr, logical.ErrEntityDisabled)
c.stateLock.RUnlock()
return retErr
}
// Attempt to use the token (decrement num_uses) // Attempt to use the token (decrement num_uses)
if te != nil { if te != nil {
te, err = c.tokenStore.UseToken(ctx, te) te, err = c.tokenStore.UseToken(ctx, te)

View File

@ -434,7 +434,7 @@ var aliasHelp = map[string][2]string{
"", "",
}, },
"alias-id-list": { "alias-id-list": {
"List all the entity IDs.", "List all the alias IDs.",
"", "",
}, },
} }

View File

@ -45,6 +45,10 @@ vault <command> <path> metadata=key1=value1 metadata=key2=value2
Type: framework.TypeCommaStringSlice, Type: framework.TypeCommaStringSlice,
Description: "Policies to be tied to the entity.", Description: "Policies to be tied to the entity.",
}, },
"disabled": {
Type: framework.TypeBool,
Description: "If set true, tokens tied to this identity will not be able to be used (but will not be revoked).",
},
}, },
Callbacks: map[logical.Operation]framework.OperationFunc{ Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: i.pathEntityRegister(), logical.UpdateOperation: i.pathEntityRegister(),
@ -76,6 +80,10 @@ vault <command> <path> metadata=key1=value1 metadata=key2=value2
Type: framework.TypeCommaStringSlice, Type: framework.TypeCommaStringSlice,
Description: "Policies to be tied to the entity.", Description: "Policies to be tied to the entity.",
}, },
"disabled": {
Type: framework.TypeBool,
Description: "If set true, tokens tied to this identity will not be able to be used (but will not be revoked).",
},
}, },
Callbacks: map[logical.Operation]framework.OperationFunc{ Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: i.pathEntityIDUpdate(), logical.UpdateOperation: i.pathEntityIDUpdate(),
@ -354,6 +362,11 @@ func (i *IdentityStore) handleEntityUpdateCommon(req *logical.Request, d *framew
entity.Policies = entityPoliciesRaw.([]string) entity.Policies = entityPoliciesRaw.([]string)
} }
disabledRaw, ok := d.GetOk("disabled")
if ok {
entity.Disabled = disabledRaw.(bool)
}
// Get the name // Get the name
entityName := d.Get("name").(string) entityName := d.Get("name").(string)
if entityName != "" { if entityName != "" {
@ -434,6 +447,7 @@ func (i *IdentityStore) handleEntityReadCommon(entity *identity.Entity) (*logica
respData["metadata"] = entity.Metadata respData["metadata"] = entity.Metadata
respData["merged_entity_ids"] = entity.MergedEntityIDs respData["merged_entity_ids"] = entity.MergedEntityIDs
respData["policies"] = entity.Policies respData["policies"] = entity.Policies
respData["disabled"] = entity.Disabled
// Convert protobuf timestamp into RFC3339 format // Convert protobuf timestamp into RFC3339 format
respData["creation_time"] = ptypes.TimestampString(entity.CreationTime) respData["creation_time"] = ptypes.TimestampString(entity.CreationTime)

View File

@ -0,0 +1,176 @@
package vault_test
import (
"strings"
"testing"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/builtin/credential/approle"
vaulthttp "github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/vault"
)
func TestIdentityStore_EntityDisabled(t *testing.T) {
// Use a TestCluster and the approle backend to get a token and entity for testing
coreConfig := &vault.CoreConfig{
CredentialBackends: map[string]logical.Factory{
"approle": approle.Factory,
},
}
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})
cluster.Start()
defer cluster.Cleanup()
core := cluster.Cores[0].Core
vault.TestWaitActive(t, core)
client := cluster.Cores[0].Client
// Mount the auth backend
err := client.Sys().EnableAuthWithOptions("approle", &api.EnableAuthOptions{
Type: "approle",
})
if err != nil {
t.Fatal(err)
}
// Tune the mount
err = client.Sys().TuneMount("auth/approle", api.MountConfigInput{
DefaultLeaseTTL: "5m",
MaxLeaseTTL: "5m",
})
if err != nil {
t.Fatal(err)
}
// Create role
resp, err := client.Logical().Write("auth/approle/role/role-period", map[string]interface{}{
"period": "5m",
})
if err != nil {
t.Fatal(err)
}
// Get role_id
resp, err = client.Logical().Read("auth/approle/role/role-period/role-id")
if err != nil {
t.Fatal(err)
}
if resp == nil {
t.Fatal("expected a response for fetching the role-id")
}
roleID := resp.Data["role_id"]
// Get secret_id
resp, err = client.Logical().Write("auth/approle/role/role-period/secret-id", map[string]interface{}{})
if err != nil {
t.Fatal(err)
}
if resp == nil {
t.Fatal("expected a response for fetching the secret-id")
}
secretID := resp.Data["secret_id"]
// Login
resp, err = client.Logical().Write("auth/approle/login", map[string]interface{}{
"role_id": roleID,
"secret_id": secretID,
})
if err != nil {
t.Fatal(err)
}
if resp == nil {
t.Fatal("expected a response for login")
}
if resp.Auth == nil {
t.Fatal("expected auth object from response")
}
if resp.Auth.ClientToken == "" {
t.Fatal("expected a client token")
}
roleToken := resp.Auth.ClientToken
client.SetToken(roleToken)
resp, err = client.Auth().Token().LookupSelf()
if err != nil {
t.Fatal(err)
}
if resp == nil {
t.Fatal("expected a response for token lookup")
}
entityIDRaw, ok := resp.Data["entity_id"]
if !ok {
t.Fatal("expected an entity ID")
}
entityID, ok := entityIDRaw.(string)
if !ok {
t.Fatal("entity_id not a string")
}
client.SetToken(cluster.RootToken)
resp, err = client.Logical().Write("identity/entity/id/"+entityID, map[string]interface{}{
"disabled": true,
})
if err != nil {
t.Fatal(err)
}
// This call should now fail
client.SetToken(roleToken)
resp, err = client.Auth().Token().LookupSelf()
if err == nil {
t.Fatalf("expected error, got %#v", *resp)
}
if !strings.Contains(err.Error(), logical.ErrEntityDisabled.Error()) {
t.Fatalf("expected to see entity disabled error, got %v", err)
}
// Attempting to get a new token should also now fail
client.SetToken("")
resp, err = client.Logical().Write("auth/approle/login", map[string]interface{}{
"role_id": roleID,
"secret_id": secretID,
})
if err == nil {
t.Fatalf("expected error, got %#v", *resp)
}
if !strings.Contains(err.Error(), logical.ErrEntityDisabled.Error()) {
t.Fatalf("expected to see entity disabled error, got %v", err)
}
client.SetToken(cluster.RootToken)
resp, err = client.Logical().Write("identity/entity/id/"+entityID, map[string]interface{}{
"disabled": false,
})
if err != nil {
t.Fatal(err)
}
client.SetToken(roleToken)
resp, err = client.Auth().Token().LookupSelf()
if err != nil {
t.Fatal(err)
}
// Getting a new token should now work again too
client.SetToken("")
resp, err = client.Logical().Write("auth/approle/login", map[string]interface{}{
"role_id": roleID,
"secret_id": secretID,
})
if err != nil {
t.Fatal(err)
}
if resp == nil {
t.Fatal("expected a response for login")
}
if resp.Auth == nil {
t.Fatal("expected auth object from response")
}
if resp.Auth.ClientToken == "" {
t.Fatal("expected a client token")
}
}

View File

@ -68,7 +68,7 @@ func groupAliasPaths(i *IdentityStore) []*framework.Path {
}, },
HelpSynopsis: strings.TrimSpace(groupAliasHelp["group-alias-by-id"][0]), HelpSynopsis: strings.TrimSpace(groupAliasHelp["group-alias-by-id"][0]),
HelpDescription: strings.TrimSpace(groupHelp["group-alias-by-id"][1]), HelpDescription: strings.TrimSpace(groupAliasHelp["group-alias-by-id"][1]),
}, },
{ {
Pattern: "group-alias/id/?$", Pattern: "group-alias/id/?$",
@ -76,8 +76,8 @@ func groupAliasPaths(i *IdentityStore) []*framework.Path {
logical.ListOperation: i.pathGroupAliasIDList(), logical.ListOperation: i.pathGroupAliasIDList(),
}, },
HelpSynopsis: strings.TrimSpace(entityHelp["group-alias-id-list"][0]), HelpSynopsis: strings.TrimSpace(groupAliasHelp["group-alias-id-list"][0]),
HelpDescription: strings.TrimSpace(entityHelp["group-alias-id-list"][1]), HelpDescription: strings.TrimSpace(groupAliasHelp["group-alias-id-list"][1]),
}, },
} }
} }
@ -285,7 +285,7 @@ var groupAliasHelp = map[string][2]string{
"", "",
}, },
"group-alias-id-list": { "group-alias-id-list": {
"List all the entity IDs.", "List all the group alias IDs.",
"", "",
}, },
} }

View File

@ -115,8 +115,8 @@ vault <command> <path> metadata=key1=value1 metadata=key2=value2
logical.ListOperation: i.pathGroupIDList(), logical.ListOperation: i.pathGroupIDList(),
}, },
HelpSynopsis: strings.TrimSpace(entityHelp["group-id-list"][0]), HelpSynopsis: strings.TrimSpace(groupHelp["group-id-list"][0]),
HelpDescription: strings.TrimSpace(entityHelp["group-id-list"][1]), HelpDescription: strings.TrimSpace(groupHelp["group-id-list"][1]),
}, },
} }
} }

View File

@ -0,0 +1,235 @@
package vault_test
import (
"testing"
"github.com/hashicorp/vault/api"
vaulthttp "github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/vault"
credLdap "github.com/hashicorp/vault/builtin/credential/ldap"
)
// Testing the fix for GH-4351
func TestIdentityStore_ExternalGroupMembershipsAcrossMounts(t *testing.T) {
coreConfig := &vault.CoreConfig{
CredentialBackends: map[string]logical.Factory{
"ldap": credLdap.Factory,
},
}
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})
cluster.Start()
defer cluster.Cleanup()
core := cluster.Cores[0].Core
vault.TestWaitActive(t, core)
client := cluster.Cores[0].Client
// Enable the first LDAP auth
err := client.Sys().EnableAuthWithOptions("ldap", &api.EnableAuthOptions{
Type: "ldap",
})
if err != nil {
t.Fatal(err)
}
// Extract out the mount accessor for LDAP auth
auths, err := client.Sys().ListAuth()
if err != nil {
t.Fatal(err)
}
ldapMountAccessor1 := auths["ldap/"].Accessor
// Configure LDAP auth
_, err = client.Logical().Write("auth/ldap/config", map[string]interface{}{
"url": "ldap://ldap.forumsys.com",
"userattr": "uid",
"userdn": "dc=example,dc=com",
"groupdn": "dc=example,dc=com",
"binddn": "cn=read-only-admin,dc=example,dc=com",
})
if err != nil {
t.Fatal(err)
}
// Create a group in LDAP auth
_, err = client.Logical().Write("auth/ldap/groups/testgroup1", map[string]interface{}{
"policies": "testgroup1-policy",
})
if err != nil {
t.Fatal(err)
}
// Tie the group to a user
_, err = client.Logical().Write("auth/ldap/users/tesla", map[string]interface{}{
"policies": "default",
"groups": "testgroup1",
})
if err != nil {
t.Fatal(err)
}
// Create an external group
secret, err := client.Logical().Write("identity/group", map[string]interface{}{
"type": "external",
})
if err != nil {
t.Fatal(err)
}
ldapExtGroupID1 := secret.Data["id"].(string)
// Associate a group from LDAP auth as a group-alias in the external group
_, err = client.Logical().Write("identity/group-alias", map[string]interface{}{
"name": "testgroup1",
"mount_accessor": ldapMountAccessor1,
"canonical_id": ldapExtGroupID1,
})
if err != nil {
t.Fatal(err)
}
// Login using LDAP
secret, err = client.Logical().Write("auth/ldap/login/tesla", map[string]interface{}{
"password": "password",
})
if err != nil {
t.Fatal(err)
}
ldapClientToken := secret.Auth.ClientToken
//
// By now, the entity ID of the token should be automatically added as a
// member in the external group.
//
// Extract the entity ID of the token
secret, err = client.Logical().Read("auth/token/lookup/" + ldapClientToken)
if err != nil {
t.Fatal(err)
}
entityID := secret.Data["entity_id"].(string)
// Enable another LDAP auth mount
err = client.Sys().EnableAuthWithOptions("ldap2", &api.EnableAuthOptions{
Type: "ldap",
})
if err != nil {
t.Fatal(err)
}
// Extract the mount accessor
auths, err = client.Sys().ListAuth()
if err != nil {
t.Fatal(err)
}
ldapMountAccessor2 := auths["ldap2/"].Accessor
// Create an entity-alias asserting that the user "tesla" from the first
// and second LDAP mounts as the same.
_, err = client.Logical().Write("identity/entity-alias", map[string]interface{}{
"name": "tesla",
"mount_accessor": ldapMountAccessor2,
"canonical_id": entityID,
})
if err != nil {
t.Fatal(err)
}
// Configure second LDAP auth
_, err = client.Logical().Write("auth/ldap2/config", map[string]interface{}{
"url": "ldap://ldap.forumsys.com",
"userattr": "uid",
"userdn": "dc=example,dc=com",
"groupdn": "dc=example,dc=com",
"binddn": "cn=read-only-admin,dc=example,dc=com",
})
if err != nil {
t.Fatal(err)
}
// Create a group in second LDAP auth
_, err = client.Logical().Write("auth/ldap2/groups/testgroup2", map[string]interface{}{
"policies": "testgroup2-policy",
})
if err != nil {
t.Fatal(err)
}
// Create a user in second LDAP auth
_, err = client.Logical().Write("auth/ldap2/users/tesla", map[string]interface{}{
"policies": "default",
"groups": "testgroup2",
})
if err != nil {
t.Fatal(err)
}
// Create another external group
secret, err = client.Logical().Write("identity/group", map[string]interface{}{
"type": "external",
})
if err != nil {
t.Fatal(err)
}
ldapExtGroupID2 := secret.Data["id"].(string)
// Create a group-alias tying the external group to "testgroup2" group in second LDAP
_, err = client.Logical().Write("identity/group-alias", map[string]interface{}{
"name": "testgroup2",
"mount_accessor": ldapMountAccessor2,
"canonical_id": ldapExtGroupID2,
})
if err != nil {
t.Fatal(err)
}
// Login using second LDAP
_, err = client.Logical().Write("auth/ldap2/login/tesla", map[string]interface{}{
"password": "password",
})
if err != nil {
t.Fatal(err)
}
//
// By now the same entity ID of the token from first LDAP should have been
// added as a member of the second external group.
//
// Check that entityID is present in both the external groups
secret, err = client.Logical().Read("identity/group/id/" + ldapExtGroupID1)
if err != nil {
t.Fatal(err)
}
extGroup1Entities := secret.Data["member_entity_ids"].([]interface{})
found := false
for _, item := range extGroup1Entities {
if item.(string) == entityID {
found = true
break
}
}
if !found {
t.Fatalf("missing entity ID %q first external group with ID %q", entityID, ldapExtGroupID1)
}
secret, err = client.Logical().Read("identity/group/id/" + ldapExtGroupID2)
if err != nil {
t.Fatal(err)
}
extGroup2Entities := secret.Data["member_entity_ids"].([]interface{})
found = false
for _, item := range extGroup2Entities {
if item.(string) == entityID {
found = true
break
}
}
if !found {
t.Fatalf("missing entity ID %q first external group with ID %q", entityID, ldapExtGroupID2)
}
}

View File

@ -2309,6 +2309,11 @@ func (i *IdentityStore) refreshExternalGroupMembershipsByEntityID(entityID strin
return err return err
} }
mountAccessor := ""
if len(groupAliases) != 0 {
mountAccessor = groupAliases[0].MountAccessor
}
var newGroups []*identity.Group var newGroups []*identity.Group
for _, alias := range groupAliases { for _, alias := range groupAliases {
aliasByFactors, err := i.MemDBAliasByFactors(alias.MountAccessor, alias.Name, true, true) aliasByFactors, err := i.MemDBAliasByFactors(alias.MountAccessor, alias.Name, true, true)
@ -2352,6 +2357,12 @@ func (i *IdentityStore) refreshExternalGroupMembershipsByEntityID(entityID strin
continue continue
} }
// If the external group is from a different mount, don't remove the
// entity ID from it.
if mountAccessor != "" && group.Alias.MountAccessor != mountAccessor {
continue
}
i.logger.Debug("removing member entity ID from external group", "member_entity_id", entityID, "group_id", group.ID) i.logger.Debug("removing member entity ID from external group", "member_entity_id", entityID, "group_id", group.ID)
group.MemberEntityIDs = strutil.StrListDelete(group.MemberEntityIDs, entityID) group.MemberEntityIDs = strutil.StrListDelete(group.MemberEntityIDs, entityID)

View File

@ -684,6 +684,9 @@ func NewSystemBackend(core *Core, logger log.Logger) *SystemBackend {
logical.UpdateOperation: b.handlePolicySet, logical.UpdateOperation: b.handlePolicySet,
logical.DeleteOperation: b.handlePolicyDelete, logical.DeleteOperation: b.handlePolicyDelete,
}, },
HelpSynopsis: strings.TrimSpace(sysHelp["policy"][0]),
HelpDescription: strings.TrimSpace(sysHelp["policy"][1]),
}, },
&framework.Path{ &framework.Path{
@ -1096,6 +1099,8 @@ func NewSystemBackend(core *Core, logger log.Logger) *SystemBackend {
logical.DeleteOperation: b.handleRawDelete, logical.DeleteOperation: b.handleRawDelete,
logical.ListOperation: b.handleRawList, logical.ListOperation: b.handleRawList,
}, },
HelpSynopsis: strings.TrimSpace(sysHelp["raw"][0]),
HelpDescription: strings.TrimSpace(sysHelp["raw"][1]),
}) })
} }
@ -4039,4 +4044,8 @@ This path responds to the following HTTP methods.
"passthrough_request_headers": { "passthrough_request_headers": {
"A list of headers to whitelist and pass from the request to the backend.", "A list of headers to whitelist and pass from the request to the backend.",
}, },
"raw": {
"Write, Read, and Delete data directly in the Storage backend.",
"",
},
} }

View File

@ -204,7 +204,7 @@ func (c *Core) handleRequest(ctx context.Context, req *logical.Request) (retResp
// return invalid request so that the status codes can be correct // return invalid request so that the status codes can be correct
errType := logical.ErrInvalidRequest errType := logical.ErrInvalidRequest
switch ctErr { switch ctErr {
case ErrInternalError, logical.ErrPermissionDenied: case ErrInternalError, logical.ErrPermissionDenied, logical.ErrEntityDisabled:
errType = ctErr errType = ctErr
} }
@ -519,6 +519,10 @@ func (c *Core) handleLoginRequest(ctx context.Context, req *logical.Request) (re
return nil, nil, fmt.Errorf("failed to create an entity for the authenticated alias") return nil, nil, fmt.Errorf("failed to create an entity for the authenticated alias")
} }
if entity.Disabled {
return nil, nil, logical.ErrEntityDisabled
}
auth.EntityID = entity.ID auth.EntityID = entity.ID
if auth.GroupAliases != nil { if auth.GroupAliases != nil {
err = c.identityStore.refreshExternalGroupMembershipsByEntityID(auth.EntityID, auth.GroupAliases) err = c.identityStore.refreshExternalGroupMembershipsByEntityID(auth.EntityID, auth.GroupAliases)

View File

@ -18,6 +18,7 @@ import (
"github.com/hashicorp/go-multierror" "github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-uuid" "github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/helper/consts" "github.com/hashicorp/vault/helper/consts"
"github.com/hashicorp/vault/helper/identity"
"github.com/hashicorp/vault/helper/jsonutil" "github.com/hashicorp/vault/helper/jsonutil"
"github.com/hashicorp/vault/helper/locksutil" "github.com/hashicorp/vault/helper/locksutil"
"github.com/hashicorp/vault/helper/parseutil" "github.com/hashicorp/vault/helper/parseutil"
@ -104,6 +105,8 @@ type TokenStore struct {
salt *salt.Salt salt *salt.Salt
tidyLock int64 tidyLock int64
identityPoliciesDeriverFunc func(string) (*identity.Entity, []string, error)
} }
// NewTokenStore is used to construct a token store that is // NewTokenStore is used to construct a token store that is
@ -114,11 +117,12 @@ func NewTokenStore(ctx context.Context, logger log.Logger, c *Core, config *logi
// Initialize the store // Initialize the store
t := &TokenStore{ t := &TokenStore{
view: view, view: view,
cubbyholeDestroyer: destroyCubbyhole, cubbyholeDestroyer: destroyCubbyhole,
logger: logger, logger: logger,
tokenLocks: locksutil.CreateLocks(), tokenLocks: locksutil.CreateLocks(),
saltLock: sync.RWMutex{}, saltLock: sync.RWMutex{},
identityPoliciesDeriverFunc: c.fetchEntityAndDerivedPolicies,
} }
if c.policyStore != nil { if c.policyStore != nil {
@ -2204,6 +2208,16 @@ func (ts *TokenStore) handleLookup(ctx context.Context, req *logical.Request, da
resp.Data["issue_time"] = leaseTimes.IssueTime resp.Data["issue_time"] = leaseTimes.IssueTime
} }
if out.EntityID != "" {
_, identityPolicies, err := ts.identityPoliciesDeriverFunc(out.EntityID)
if err != nil {
return nil, err
}
if len(identityPolicies) != 0 {
resp.Data["identity_policies"] = identityPolicies
}
}
if urltoken { if urltoken {
resp.AddWarning(`Using a token in the path is unsafe as the token can be logged in many places. Please use POST or PUT with the token passed in via the "token" parameter.`) resp.AddWarning(`Using a token in the path is unsafe as the token can be logged in many places. Please use POST or PUT with the token passed in via the "token" parameter.`)
} }

View File

@ -0,0 +1,219 @@
package vault_test
import (
"reflect"
"sort"
"testing"
"github.com/hashicorp/vault/api"
credLdap "github.com/hashicorp/vault/builtin/credential/ldap"
vaulthttp "github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/vault"
)
func TestTokenStore_IdentityPolicies(t *testing.T) {
coreConfig := &vault.CoreConfig{
CredentialBackends: map[string]logical.Factory{
"ldap": credLdap.Factory,
},
}
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})
cluster.Start()
defer cluster.Cleanup()
core := cluster.Cores[0].Core
vault.TestWaitActive(t, core)
client := cluster.Cores[0].Client
// Enable LDAP auth
err := client.Sys().EnableAuthWithOptions("ldap", &api.EnableAuthOptions{
Type: "ldap",
})
if err != nil {
t.Fatal(err)
}
// Configure LDAP auth
_, err = client.Logical().Write("auth/ldap/config", map[string]interface{}{
"url": "ldap://ldap.forumsys.com",
"userattr": "uid",
"userdn": "dc=example,dc=com",
"groupdn": "dc=example,dc=com",
"binddn": "cn=read-only-admin,dc=example,dc=com",
})
if err != nil {
t.Fatal(err)
}
// Create group in LDAP auth
_, err = client.Logical().Write("auth/ldap/groups/testgroup1", map[string]interface{}{
"policies": "testgroup1-policy",
})
if err != nil {
t.Fatal(err)
}
// Create user in LDAP auth
_, err = client.Logical().Write("auth/ldap/users/tesla", map[string]interface{}{
"policies": "default",
"groups": "testgroup1",
})
if err != nil {
t.Fatal(err)
}
// Login using LDAP
secret, err := client.Logical().Write("auth/ldap/login/tesla", map[string]interface{}{
"password": "password",
})
if err != nil {
t.Fatal(err)
}
ldapClientToken := secret.Auth.ClientToken
// At this point there shouldn't be any identity policy on the token
secret, err = client.Logical().Read("auth/token/lookup/" + ldapClientToken)
if err != nil {
t.Fatal(err)
}
_, ok := secret.Data["identity_policies"]
if ok {
t.Fatalf("identity_policies should not have been set")
}
// Extract the entity ID of the token and set some policies on the entity
entityID := secret.Data["entity_id"].(string)
_, err = client.Logical().Write("identity/entity/id/"+entityID, map[string]interface{}{
"policies": []string{
"entity_policy_1",
"entity_policy_2",
},
})
if err != nil {
t.Fatal(err)
}
// Lookup the token and expect entity policies on the token
secret, err = client.Logical().Read("auth/token/lookup/" + ldapClientToken)
if err != nil {
t.Fatal(err)
}
identityPolicies := secret.Data["identity_policies"].([]interface{})
var actualPolicies []string
for _, item := range identityPolicies {
actualPolicies = append(actualPolicies, item.(string))
}
sort.Strings(actualPolicies)
expectedPolicies := []string{
"entity_policy_1",
"entity_policy_2",
}
sort.Strings(expectedPolicies)
if !reflect.DeepEqual(expectedPolicies, actualPolicies) {
t.Fatalf("bad: identity policies; expected: %#v\nactual: %#v", expectedPolicies, actualPolicies)
}
// Create identity group and add entity as its member
secret, err = client.Logical().Write("identity/group", map[string]interface{}{
"policies": []string{
"group_policy_1",
"group_policy_2",
},
"member_entity_ids": []string{
entityID,
},
})
if err != nil {
t.Fatal(err)
}
// Lookup token and expect both entity and group policies on the token
secret, err = client.Logical().Read("auth/token/lookup/" + ldapClientToken)
if err != nil {
t.Fatal(err)
}
identityPolicies = secret.Data["identity_policies"].([]interface{})
actualPolicies = nil
for _, item := range identityPolicies {
actualPolicies = append(actualPolicies, item.(string))
}
sort.Strings(actualPolicies)
expectedPolicies = []string{
"entity_policy_1",
"entity_policy_2",
"group_policy_1",
"group_policy_2",
}
sort.Strings(expectedPolicies)
if !reflect.DeepEqual(expectedPolicies, actualPolicies) {
t.Fatalf("bad: identity policies; expected: %#v\nactual: %#v", expectedPolicies, actualPolicies)
}
// Create an external group and renew the token. This should add external
// group policies to the token.
auths, err := client.Sys().ListAuth()
if err != nil {
t.Fatal(err)
}
ldapMountAccessor1 := auths["ldap/"].Accessor
// Create an external group
secret, err = client.Logical().Write("identity/group", map[string]interface{}{
"type": "external",
"policies": []string{
"external_group_policy_1",
"external_group_policy_2",
},
})
if err != nil {
t.Fatal(err)
}
ldapExtGroupID1 := secret.Data["id"].(string)
// Associate a group from LDAP auth as a group-alias in the external group
_, err = client.Logical().Write("identity/group-alias", map[string]interface{}{
"name": "testgroup1",
"mount_accessor": ldapMountAccessor1,
"canonical_id": ldapExtGroupID1,
})
if err != nil {
t.Fatal(err)
}
// Renew token to refresh external group memberships
secret, err = client.Auth().Token().Renew(ldapClientToken, 10)
if err != nil {
t.Fatal(err)
}
// Lookup token and expect entity, group and external group policies on the
// token
secret, err = client.Logical().Read("auth/token/lookup/" + ldapClientToken)
if err != nil {
t.Fatal(err)
}
identityPolicies = secret.Data["identity_policies"].([]interface{})
actualPolicies = nil
for _, item := range identityPolicies {
actualPolicies = append(actualPolicies, item.(string))
}
sort.Strings(actualPolicies)
expectedPolicies = []string{
"entity_policy_1",
"entity_policy_2",
"group_policy_1",
"group_policy_2",
"external_group_policy_1",
"external_group_policy_2",
}
sort.Strings(expectedPolicies)
if !reflect.DeepEqual(expectedPolicies, actualPolicies) {
t.Fatalf("bad: identity policies; expected: %#v\nactual: %#v", expectedPolicies, actualPolicies)
}
}

View File

@ -216,10 +216,17 @@ type ServiceConfig struct {
ServicePath string ServicePath string
} }
func (r *iamResourceImpl) SetIamPolicyRequest(p *Policy) (*http.Request, error) { func (r *iamResourceImpl) SetIamPolicyRequest(p *Policy) (req *http.Request, err error) {
data := struct { var data interface{}
Policy *Policy `json:"policy,omitempty"` switch strings.ToLower(r.config.Service.Name) {
}{Policy: p} // GCS uses a different payload than every other API.
case "storage":
data = p
default:
data = struct {
Policy *Policy `json:"policy,omitempty"`
}{Policy: p}
}
buf, err := googleapi.WithoutDataWrapper.JSONReader(data) buf, err := googleapi.WithoutDataWrapper.JSONReader(data)
if err != nil { if err != nil {
@ -240,6 +247,12 @@ func (r *iamResourceImpl) constructRequest(httpMtd *HttpMethodCfg, data io.Reade
return nil, err return nil, err
} }
if req.Header == nil {
req.Header = make(http.Header)
}
if data != nil {
req.Header.Set("Content-Type", "application/json")
}
replacementMap := make(map[string]string) replacementMap := make(map[string]string)
for cId, replaceK := range httpMtd.ReplacementKeys { for cId, replaceK := range httpMtd.ReplacementKeys {
rId, ok := r.relativeId.IdTuples[cId] rId, ok := r.relativeId.IdTuples[cId]

View File

@ -157,9 +157,9 @@ func (b *backend) serviceAccountPolicyRollback(ctx context.Context, req *logical
return err return err
} }
// Take our any bindings still being used by this role set from roles being removed. // Take out any bindings still being used by this role set from roles being removed.
rolesToRemove := util.ToSet(entry.Roles) rolesToRemove := util.ToSet(entry.Roles)
if rs.AccountId.ResourceName() == entry.AccountId.ResourceName() { if rs != nil && rs.AccountId.ResourceName() == entry.AccountId.ResourceName() {
currRoles, ok := rs.Bindings[entry.Resource] currRoles, ok := rs.Bindings[entry.Resource]
if ok { if ok {
rolesToRemove = rolesToRemove.Sub(currRoles) rolesToRemove = rolesToRemove.Sub(currRoles)

16
vendor/vendor.json vendored
View File

@ -1315,22 +1315,22 @@
"revisionTime": "2018-04-03T19:54:48Z" "revisionTime": "2018-04-03T19:54:48Z"
}, },
{ {
"checksumSHA1": "0wzWab/JiPNjFvIx3yg5ztABJ7M=", "checksumSHA1": "RPIFaxCjd6lchN99dcfhJ0vqqTI=",
"path": "github.com/hashicorp/vault-plugin-secrets-gcp/plugin", "path": "github.com/hashicorp/vault-plugin-secrets-gcp/plugin",
"revision": "963249c4107fa592ba935ccdddd07155214a83bd", "revision": "b5ca08100f178f8c3eb138c4f5c4a3d039ed173e",
"revisionTime": "2018-04-03T22:22:59Z" "revisionTime": "2018-04-17T17:32:19Z"
}, },
{ {
"checksumSHA1": "sV9w7yCitfGr2/4RjdKkdZjYAmA=", "checksumSHA1": "1xWWeopXV4tbHTrVM1eFuH8TCuo=",
"path": "github.com/hashicorp/vault-plugin-secrets-gcp/plugin/iamutil", "path": "github.com/hashicorp/vault-plugin-secrets-gcp/plugin/iamutil",
"revision": "963249c4107fa592ba935ccdddd07155214a83bd", "revision": "b5ca08100f178f8c3eb138c4f5c4a3d039ed173e",
"revisionTime": "2018-04-03T22:22:59Z" "revisionTime": "2018-04-17T17:32:19Z"
}, },
{ {
"checksumSHA1": "81kYL49zTBoj1NYczxB2Xbr2d6Y=", "checksumSHA1": "81kYL49zTBoj1NYczxB2Xbr2d6Y=",
"path": "github.com/hashicorp/vault-plugin-secrets-gcp/plugin/util", "path": "github.com/hashicorp/vault-plugin-secrets-gcp/plugin/util",
"revision": "963249c4107fa592ba935ccdddd07155214a83bd", "revision": "b5ca08100f178f8c3eb138c4f5c4a3d039ed173e",
"revisionTime": "2018-04-03T22:22:59Z" "revisionTime": "2018-04-17T17:32:19Z"
}, },
{ {
"checksumSHA1": "m3cQgQrCSuWHiPA339FaZU6LuHU=", "checksumSHA1": "m3cQgQrCSuWHiPA339FaZU6LuHU=",

View File

@ -662,8 +662,7 @@ list in order to satisfy that constraint.
- `period` `(string: "")` - If set, indicates that the token generated using - `period` `(string: "")` - If set, indicates that the token generated using
this role should never expire. The token should be renewed within the duration this role should never expire. The token should be renewed within the duration
specified by this value. At each renewal, the token's TTL will be set to the specified by this value. At each renewal, the token's TTL will be set to the
value of this parameter. The maximum allowed lifetime of tokens issued using value of this parameter.
this role.
- `policies` `(array: [])` - Policies to be set on tokens issued using this - `policies` `(array: [])` - Policies to be set on tokens issued using this
role. role.
- `allow_instance_migration` `(bool: false)` - If set, allows migration of the - `allow_instance_migration` `(bool: false)` - If set, allows migration of the

View File

@ -180,18 +180,30 @@ $ curl \
```json ```json
{ {
"data": { "data": {
"id": "ClientToken", "accessor": "8609694a-cdbc-db9b-d345-e782dbb562ed",
"policies": [ "creation_time": 1523979354,
"web", "creation_ttl": 2764800,
"stage" "display_name": "ldap2-tesla",
], "entity_id": "7d2e3179-f69b-450c-7179-ac8ee8bd8ca9",
"path": "auth/github/login", "expire_time": "2018-05-19T11:35:54.466476215-04:00",
"explicit_max_ttl": 0,
"id": "cf64a70f-3a12-3f6c-791d-6cef6d390eed",
"identity_policies": [
"dev-group-policy"
],
"issue_time": "2018-04-17T11:35:54.466476078-04:00",
"meta": { "meta": {
"user": "armon", "username": "tesla"
"organization": "hashicorp"
}, },
"display_name": "github-armon", "num_uses": 0,
"num_uses": 0 "orphan": true,
"path": "auth/ldap2/login/tesla",
"policies": [
"default",
"testgroup2-policy"
],
"renewable": true,
"ttl": 2764790
} }
} }
``` ```
@ -217,18 +229,30 @@ $ curl \
```json ```json
{ {
"data": { "data": {
"id": "ClientToken", "accessor": "8609694a-cdbc-db9b-d345-e782dbb562ed",
"policies": [ "creation_time": 1523979354,
"web", "creation_ttl": 2764800,
"stage" "display_name": "ldap2-tesla",
], "entity_id": "7d2e3179-f69b-450c-7179-ac8ee8bd8ca9",
"path": "auth/github/login", "expire_time": "2018-05-19T11:35:54.466476215-04:00",
"explicit_max_ttl": 0,
"id": "cf64a70f-3a12-3f6c-791d-6cef6d390eed",
"identity_policies": [
"dev-group-policy"
],
"issue_time": "2018-04-17T11:35:54.466476078-04:00",
"meta": { "meta": {
"user": "armon", "username": "tesla"
"organization": "hashicorp"
}, },
"display_name": "github-armon", "num_uses": 0,
"num_uses": 0 "orphan": true,
"path": "auth/ldap2/login/tesla",
"policies": [
"default",
"testgroup2-policy"
],
"renewable": true,
"ttl": 2764790
} }
} }
``` ```
@ -249,7 +273,7 @@ Returns information about the client token from the accessor.
```json ```json
{ {
"accessor": "2c84f488-2133-4ced-87b0-570f93a76830" "accessor": "8609694a-cdbc-db9b-d345-e782dbb562ed"
} }
``` ```
@ -267,25 +291,32 @@ $ curl \
```json ```json
{ {
"lease_id": "",
"renewable": false,
"lease_duration": 0,
"data": { "data": {
"creation_time": 1457533232, "accessor": "8609694a-cdbc-db9b-d345-e782dbb562ed",
"creation_time": 1523979354,
"creation_ttl": 2764800, "creation_ttl": 2764800,
"display_name": "token", "display_name": "ldap2-tesla",
"meta": null, "entity_id": "7d2e3179-f69b-450c-7179-ac8ee8bd8ca9",
"expire_time": "2018-05-19T11:35:54.466476215-04:00",
"explicit_max_ttl": 0,
"id": "",
"identity_policies": [
"dev-group-policy"
],
"issue_time": "2018-04-17T11:35:54.466476078-04:00",
"meta": {
"username": "tesla"
},
"num_uses": 0, "num_uses": 0,
"orphan": false, "orphan": true,
"path": "auth/token/create", "path": "auth/ldap2/login/tesla",
"policies": [ "policies": [
"default", "default",
"web" "testgroup2-policy"
], ],
"ttl": 2591976 "renewable": true,
}, "ttl": 2763902
"warnings": null, }
"auth": null
} }
``` ```

View File

@ -26,6 +26,9 @@ This endpoint creates or updates an Entity.
- `policies` `(list of strings: [])` Policies to be tied to the entity. - `policies` `(list of strings: [])` Policies to be tied to the entity.
- `disabled` `(bool: false)` Whether the entity is disabled. Disabled
entities' associated tokens cannot be used, but are not revoked.
### Sample Payload ### Sample Payload
```json ```json
@ -86,6 +89,7 @@ $ curl \
"data": { "data": {
"bucket_key_hash": "177553e4c58987f4cc5d7e530136c642", "bucket_key_hash": "177553e4c58987f4cc5d7e530136c642",
"creation_time": "2017-07-25T20:29:22.614756844Z", "creation_time": "2017-07-25T20:29:22.614756844Z",
"disabled": false,
"id": "8d6a45e5-572f-8f13-d226-cd0d1ec57297", "id": "8d6a45e5-572f-8f13-d226-cd0d1ec57297",
"last_update_time": "2017-07-25T20:29:22.614756844Z", "last_update_time": "2017-07-25T20:29:22.614756844Z",
"metadata": { "metadata": {
@ -120,6 +124,8 @@ This endpoint is used to update an existing entity.
- `policies` `(list of strings: [])` Policies to be tied to the entity. - `policies` `(list of strings: [])` Policies to be tied to the entity.
- `disabled` `(bool: false)` Whether the entity is disabled. Disabled
entities' associated tokens cannot be used, but are not revoked.
### Sample Payload ### Sample Payload

View File

@ -222,7 +222,7 @@ can be achieved without `sudo` via `sys/mounts/auth/[auth-path]/tune`._
object. object.
- `listing_visibility` `(string: "")` - Speficies whether to show this mount - `listing_visibility` `(string: "")` - Speficies whether to show this mount
in the UI-specific listing endpoint. in the UI-specific listing endpoint. Valid values are `"unauth"` or `""`.
- `passthrough_request_headers` `(array: [])` - Comma-separated list of headers - `passthrough_request_headers` `(array: [])` - Comma-separated list of headers
to whitelist and pass from the request to the backend. to whitelist and pass from the request to the backend.

View File

@ -0,0 +1,53 @@
---
layout: "api"
page_title: "/sys/internal/ui/mounts - HTTP API"
sidebar_current: "docs-http-system-internal-ui-mounts"
description: |-
The `/sys/internal/ui/mounts` endpoint is used to manage mount listing visibility.
---
# `/sys/internal/ui/mounts`
The `/sys/internal/ui/mounts` endpoint is used to manage mount listing
visibility. The response generated by this endpoint is based on the
`listing_visibility` value on the mount, which can be set during mount time or
via mount tuning. This is currently only being used internally for the UI and is
an unauthenticated endpoint.
Due to the nature of its intended usage, there is no guarantee on backwards
compatibility for this endpoint.
## Get Available Visible Mounts
This endpoint lists all enabled auth methods.
| Method | Path | Produces |
| :----- | :------------------------ | :--------------------- |
| `GET` | `/sys/internal/ui/mounts` | `200 application/json` |
### Sample Request
```
$ curl \
http://127.0.0.1:8200/v1/sys/internal/ui/mounts
```
### Sample Response
```json
{
"auth": {
"github/": {
"description": "GitHub auth",
"type": "github"
}
},
"secret": {
"custom-secrets/": {
"description": "Custom secrets",
"type": "kv"
}
}
}
```

View File

@ -223,7 +223,7 @@ This endpoint tunes configuration parameters for a given mount point.
object. object.
- `listing_visibility` `(string: "")` - Speficies whether to show this mount - `listing_visibility` `(string: "")` - Speficies whether to show this mount
in the UI-specific listing endpoint. in the UI-specific listing endpoint. Valid values are `"unauth"` or `""`.
- `passthrough_request_headers` `(array: [])` - Comma-separated list of headers - `passthrough_request_headers` `(array: [])` - Comma-separated list of headers
to whitelist and pass from the request to the backend. to whitelist and pass from the request to the backend.

View File

@ -92,7 +92,7 @@ A single CLI command can be used to create a new OTP and invoke SSH with the
correct parameters to connect to the host. correct parameters to connect to the host.
```text ```text
$ vault ssh -role otp_key_role username@x.x.x.x $ vault ssh -role otp_key_role -mode otp username@x.x.x.x
OTP for the session is `b4d47e1b-4879-5f4e-ce5c-7988d7986f37` OTP for the session is `b4d47e1b-4879-5f4e-ce5c-7988d7986f37`
[Note: Install `sshpass` to automate typing in OTP] [Note: Install `sshpass` to automate typing in OTP]
Password: <Enter OTP> Password: <Enter OTP>
@ -101,7 +101,7 @@ Password: <Enter OTP>
The OTP will be entered automatically using `sshpass` if it is installed. The OTP will be entered automatically using `sshpass` if it is installed.
```text ```text
$ vault ssh -role otp_key_role -strict-host-key-checking=no username@x.x.x.x $ vault ssh -role otp_key_role -mode otp -strict-host-key-checking=no username@x.x.x.x
username@<IP of remote host>:~$ username@<IP of remote host>:~$
``` ```

View File

@ -46,7 +46,7 @@ $ vault kv put secret/hello foo=world excited=yes
Success! Data written to: secret/hello Success! Data written to: secret/hello
``` ```
`vault write` is a very powerful command. In addition to writing data `vault kv put` is a very powerful command. In addition to writing data
directly from the command-line, it can read values and key pairs from directly from the command-line, it can read values and key pairs from
`STDIN` as well as files. For more information, see the `STDIN` as well as files. For more information, see the
[command documentation](/docs/commands/index.html). [command documentation](/docs/commands/index.html).

View File

@ -219,6 +219,9 @@
<li<%= sidebar_current("docs-http-system-init") %>> <li<%= sidebar_current("docs-http-system-init") %>>
<a href="/api/system/init.html"><tt>/sys/init</tt></a> <a href="/api/system/init.html"><tt>/sys/init</tt></a>
</li> </li>
<li<%= sidebar_current("docs-http-system-internal-ui-mounts") %>>
<a href="/api/system/internal-ui-mounts.html"><tt>/sys/internal/ui/mounts</tt></a>
</li>
<li<%= sidebar_current("docs-http-system-key-status") %>> <li<%= sidebar_current("docs-http-system-key-status") %>>
<a href="/api/system/key-status.html"><tt>/sys/key-status</tt></a> <a href="/api/system/key-status.html"><tt>/sys/key-status</tt></a>
</li> </li>