diff --git a/cmd/security-file-token-provider/res/token-config.json b/cmd/security-file-token-provider/res/token-config.json index 3dd311cb78..731d640ca2 100644 --- a/cmd/security-file-token-provider/res/token-config.json +++ b/cmd/security-file-token-provider/res/token-config.json @@ -32,22 +32,61 @@ "support-scheduler": { "edgex_use_defaults": true }, - "security-secretstore-setup": { + "security-proxy-auth": { + "edgex_use_defaults": true + }, + "security-proxy-setup": { "edgex_use_defaults": true, "custom_policy": { "path": { - "secret/edgex/security-proxy-setup/kong-tls": { + "identity/entity/name": { + "capabilities": [ + "list" + ] + }, + "identity/entity/name/*": { "capabilities": [ - "list", "read", "create", "update", "delete" ] }, - "secret/edgex/redis/*": { + "identity/entity-alias": { + "capabilities": [ + "create", + "update" + ] + }, + "identity/oidc/role": { + "capabilities": [ + "list" + ] + }, + "identity/oidc/role/*": { + "capabilities": [ + "create", + "update" + ] + }, + "auth/userpass/users/*": { + "capabilities": [ + "create", + "update" + ] + }, + "sys/auth": { + "capabilities": [ + "read" + ] + }, + "sys/policies/acl": { + "capabilities": [ + "list" + ] + }, + "sys/policies/acl/edgex-user-*": { "capabilities": [ - "list", "read", "create", "update", @@ -57,35 +96,54 @@ } } }, - "security-proxy-setup": { - "edgex_use_defaults": true - }, - "security-file-token-provider": { - "edgex_use_defaults": true - }, "security-spiffe-token-provider": { "edgex_use_defaults": true, "custom_policy": { "path": { - "auth/token/create": { + "identity/entity/name": { + "capabilities": [ + "list" + ] + }, + "identity/entity/name/*": { "capabilities": [ + "read", "create", "update", - "sudo" + "delete" ] }, - "auth/token/create-orphan": { + "identity/entity-alias": { "capabilities": [ "create", - "update", - "sudo" + "update" + ] + }, + "identity/oidc/role": { + "capabilities": [ + "list" ] }, - "auth/token/create/*": { + "identity/oidc/role/*": { "capabilities": [ "create", - "update", - "sudo" + "update" + ] + }, + "auth/userpass/users/*": { + "capabilities": [ + "create", + "update" + ] + }, + "sys/auth": { + "capabilities": [ + "read" + ] + }, + "sys/policies/acl": { + "capabilities": [ + "list" ] }, "sys/policies/acl/edgex-service-*": { diff --git a/cmd/security-secretstore-setup/res-file-token-provider/configuration.toml b/cmd/security-secretstore-setup/res-file-token-provider/configuration.toml index 0882321f0d..ef3a83cb59 100644 --- a/cmd/security-secretstore-setup/res-file-token-provider/configuration.toml +++ b/cmd/security-secretstore-setup/res-file-token-provider/configuration.toml @@ -14,3 +14,5 @@ ConfigFile = "res-file-token-provider/token-config.json" OutputDir = "/tmp/edgex/secrets" OutputFilename = "secrets-token.json" DefaultTokenTTL = "1h" +DefaultJWTTTL = "15m" +UserPassMountPoint = "userpass" diff --git a/cmd/security-spiffe-token-provider/res/configuration.toml b/cmd/security-spiffe-token-provider/res/configuration.toml index 52a0dd5138..ba29e40399 100644 --- a/cmd/security-spiffe-token-provider/res/configuration.toml +++ b/cmd/security-spiffe-token-provider/res/configuration.toml @@ -38,6 +38,17 @@ Port = 6379 Timeout = 5000 Type = "redisdb" + +[TokenConfig] +#PrivilegedTokenPath = "UNUSED" +#ConfigFile = "UNUSED" +#OutputDir = "UNUSED" +#OutputFilename = "UNUSED" +DefaultTokenTTL = "1h" +DefaultJWTTTL = "15m" +UserPassMountPoint = "userpass" + + [SPIFFE] EndpointSocket = "/tmp/edgex/secrets/spiffe/public/api.sock" TrustDomain = "edgexfoundry.org" diff --git a/internal/security/fileprovider/defaults.go b/internal/security/common/tokenpolicy.go similarity index 50% rename from internal/security/fileprovider/defaults.go rename to internal/security/common/tokenpolicy.go index 0f278cf1d5..ec03c26acd 100644 --- a/internal/security/fileprovider/defaults.go +++ b/internal/security/common/tokenpolicy.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2019-2023 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except // in compliance with the License. You may obtain a copy of the License at @@ -11,23 +11,30 @@ // or implied. See the License for the specific language governing permissions and limitations under // the License. // -// SPDX-License-Identifier: Apache-2.0' +// SPDX-License-Identifier: Apache-2.0 // -package fileprovider +package common -func makeDefaultTokenPolicy(serviceName string) map[string]interface{} { +func MakeDefaultTokenPolicy(serviceName string) map[string]interface{} { // protected path for secret/ - protectedPath := "secret/edgex/" + serviceName + "/*" - capabilities := []string{"create", "update", "delete", "list", "read"} - acl := map[string]interface{}{"capabilities": capabilities} + secretsPath := "secret/edgex/" + serviceName + "/*" + secretsAcl := map[string]interface{}{"capabilities": []string{"create", "update", "delete", "list", "read"}} // path for consul tokens registryCredsPath := "consul/creds/" + serviceName - registryCredsCapabilities := []string{"read"} - registryCredsACL := map[string]interface{}{"capabilities": registryCredsCapabilities} + registryCredsACL := map[string]interface{}{"capabilities": []string{"read"}} + // allow request identity JWT + jwtRequestPath := "identity/oidc/token/" + serviceName + jwtRequestACL := map[string]interface{}{"capabilities": []string{"read"}} + // allow introspect JWT + jwtIntrospectPath := "identity/oidc/introspect" + jwtIntrospectACL := map[string]interface{}{"capabilities": []string{"create", "update"}} + // access spec pathObject := map[string]interface{}{ - protectedPath: acl, + secretsPath: secretsAcl, registryCredsPath: registryCredsACL, + jwtRequestPath: jwtRequestACL, + jwtIntrospectPath: jwtIntrospectACL, } retval := map[string]interface{}{"path": pathObject} return retval @@ -40,18 +47,14 @@ func makeDefaultTokenPolicy(serviceName string) map[string]interface{} { }, "consul/creds/service-name": { "capabilities": [ "read" ] + }, + "identity/oidc/token/service-name": { + "capabilities": [ "read" ] + }, + "identity/oidc/introspect": { + "capabilities": [ "create", "update" ] } } } */ } - -func makeDefaultTokenParameters(serviceName string, defaultTTL string, defaultPeriod string) map[string]interface{} { - return map[string]interface{}{ - "display_name": serviceName, - "no_parent": true, - "ttl": defaultTTL, - "period": defaultPeriod, - "policies": []string{"edgex-service-" + serviceName}, - } -} diff --git a/internal/security/fileprovider/defaults_test.go b/internal/security/common/tokenpolicy_test.go similarity index 67% rename from internal/security/fileprovider/defaults_test.go rename to internal/security/common/tokenpolicy_test.go index baee6d041e..e26f70db96 100644 --- a/internal/security/fileprovider/defaults_test.go +++ b/internal/security/common/tokenpolicy_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2019-2023 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except // in compliance with the License. You may obtain a copy of the License at @@ -11,9 +11,9 @@ // or implied. See the License for the specific language governing permissions and limitations under // the License. // -// SPDX-License-Identifier: Apache-2.0' +// SPDX-License-Identifier: Apache-2.0 // -package fileprovider +package common import ( "encoding/json" @@ -24,7 +24,7 @@ import ( func TestDefaultTokenPolicy(t *testing.T) { // Act - policies := makeDefaultTokenPolicy("service-name") + policies := MakeDefaultTokenPolicy("service-name") // Assert bytes, err := json.Marshal(policies) @@ -39,21 +39,14 @@ func TestDefaultTokenPolicy(t *testing.T) { "consul/creds/service-name": map[string]interface{}{ "capabilities": []string{"read"}, }, + "identity/oidc/token/service-name": map[string]interface{}{ + "capabilities": []string{"read"}, + }, + "identity/oidc/introspect": map[string]interface{}{ + "capabilities": []string{"create", "update"}, + }, }, } require.Equal(t, expected, policies) } - -func TestDefaultTokenParameters(t *testing.T) { - // Act - parameters := makeDefaultTokenParameters("service-name", "1h", "1h") - - // Assert - bytes, err := json.Marshal(parameters) - require.NoError(t, err) - - expected := `{"display_name":"service-name","no_parent":true,"period":"1h","policies":["edgex-service-service-name"],"ttl":"1h"}` - actual := string(bytes) - require.Equal(t, expected, actual) -} diff --git a/internal/security/common/usermanager.go b/internal/security/common/usermanager.go new file mode 100644 index 0000000000..47f44c1195 --- /dev/null +++ b/internal/security/common/usermanager.go @@ -0,0 +1,137 @@ +// +// Copyright (c) 2023 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 +// + +package common + +import ( + "encoding/json" + "fmt" + + "github.com/edgexfoundry/go-mod-core-contracts/v3/clients/logger" + "github.com/edgexfoundry/go-mod-secrets/v3/secrets" +) + +type UserManager struct { + logger logger.LoggingClient + secretStoreClient secrets.SecretStoreClient + userPassMountPoint string // userPassMountPoint is the name of the userpass mount point, almost always "userpass" + jwtKeyName string // jwtKeyName is the key identifier of the JWT signing key (e.g. edgex-identity) + privilegedToken string // privilegedToken is a Vault token that has permissions to do a lot of stuff like below + tokenTTL string // This is the TTL of the Vault token (which is renewable) + jwtTTL string // JWT's created using the Vault token have an independent validity period +} + +func NewUserManager( + logger logger.LoggingClient, + secretStoreClient secrets.SecretStoreClient, + userPassMountPoint string, + jwtKeyName string, + privilegedToken string, + tokenTTL string, + jwtTTL string, +) *UserManager { + return &UserManager{ + logger, + secretStoreClient, + userPassMountPoint, + jwtKeyName, + privilegedToken, + tokenTTL, + jwtTTL, + } +} + +// CreatePasswordUserWithPolicy creates a vault identity with an attached policy +// using userpass authentication engine. +// username should be the name of the user or service to be created +// password should be a random password to be assigned +// policyPrefix is prefixed to username and should be "edgex-user-" or policyPrefix +// policy is a map that will be serialized as a policy attached to the identity +func (m *UserManager) CreatePasswordUserWithPolicy(username string, password string, policyPrefix string, policy map[string]interface{}) error { + + m.logger.Infof("creating policy, identity, and userpass binding for %s", username) + + // Derive the name of the attached policy and create it + + policyName := policyPrefix + username + + policyBytes, err := json.Marshal(policy) + if err != nil { + m.logger.Errorf("failed encode policy for %s: %s", username, err.Error()) + return err + } + + if err := m.secretStoreClient.InstallPolicy(m.privilegedToken, policyName, string(policyBytes)); err != nil { + m.logger.Errorf("failed to install policy %s: %s", policyName, err.Error()) + return err + } + + // Create or update underlying vault identity + identityMetadata := map[string]string{ + // we will also put a name claim in any generated JWT's + "name": username, + } + identityPolicies := []string{policyName} + identityId, err := m.secretStoreClient.CreateOrUpdateIdentity(m.privilegedToken, username, identityMetadata, identityPolicies) + if err != nil { + return err + } + if identityId == "" { + // Updating an entity doesn't return its ID (grr!), in that case, need to look it up + identityId, err = m.secretStoreClient.LookupIdentity(m.privilegedToken, username) + if err != nil { + return err + } + } + + // When logging in "default" policy is added automatically to the token and identity_policy is inherited. + err = m.secretStoreClient.CreateOrUpdateUser(m.privilegedToken, m.userPassMountPoint, username, password, m.tokenTTL, []string{}) + if err != nil { + return err + } + + authHandle, err := m.secretStoreClient.LookupAuthHandle(m.privilegedToken, m.userPassMountPoint) + if err != nil { + return err + } + + err = m.secretStoreClient.BindUserToIdentity(m.privilegedToken, identityId, authHandle, username) + if err != nil { + return err + } + + // See https://developer.hashicorm.com/vault/docs/secrets/identity/identity-token#token-contents-and-templates + // for including custom claims. We will include a claim identifying the calling EdgeX service + // We will use the OIDC standard "name" claim. + customClaims := fmt.Sprintf(`{"name": "%s"}`, username) + err = m.secretStoreClient.CreateOrUpdateIdentityRole(m.privilegedToken, username, m.jwtKeyName, customClaims, m.jwtTTL) + if err != nil { + return err + } + + return nil +} + +func (m *UserManager) DeletePasswordUser(username string) error { + + identityId, err := m.secretStoreClient.LookupIdentity(m.privilegedToken, username) + if err != nil { + return err + } + if identityId != "" { + err = m.secretStoreClient.DeleteIdentity(m.privilegedToken, username) + if err != nil { + return err + } + } + + err = m.secretStoreClient.DeleteUser(m.privilegedToken, m.userPassMountPoint, username) + if err != nil { + return err + } + + return nil +} diff --git a/internal/security/common/usermanager_test.go b/internal/security/common/usermanager_test.go new file mode 100644 index 0000000000..2dd4861909 --- /dev/null +++ b/internal/security/common/usermanager_test.go @@ -0,0 +1,7 @@ +// +// Copyright (c) 2023 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 +// + +package common diff --git a/internal/security/fileprovider/config/config.go b/internal/security/fileprovider/config/config.go index 6e4728ef91..18126d824d 100644 --- a/internal/security/fileprovider/config/config.go +++ b/internal/security/fileprovider/config/config.go @@ -1,6 +1,6 @@ /******************************************************************************* * Copyright 2019 Dell Inc. - * Copyright 2019 Intel Corporation. + * Copyright 2019-2023 Intel Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at @@ -38,6 +38,10 @@ type TokenFileProviderInfo struct { OutputFilename string // Default duration of issued tokens DefaultTokenTTL string + // Default duration of issued OIDC JWT tokens + DefaultJWTTTL string + // UserPassMountPoint is where the userpass auth engine is mounted (usually "userauth") + UserPassMountPoint string } // UpdateFromRaw converts configuration received from the registry to a service-specific configuration struct diff --git a/internal/security/fileprovider/provider.go b/internal/security/fileprovider/provider.go index 8f382e9600..ddcffbcc91 100644 --- a/internal/security/fileprovider/provider.go +++ b/internal/security/fileprovider/provider.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2019-2023 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except // in compliance with the License. You may obtain a copy of the License at @@ -11,18 +11,22 @@ // or implied. See the License for the specific language governing permissions and limitations under // the License. // -// SPDX-License-Identifier: Apache-2.0' +// SPDX-License-Identifier: Apache-2.0 // package fileprovider import ( + "context" "encoding/json" "os" "path/filepath" "strconv" + "github.com/edgexfoundry/edgex-go/internal/security/common" + securityCommon "github.com/edgexfoundry/edgex-go/internal/security/common" "github.com/edgexfoundry/edgex-go/internal/security/fileprovider/config" + "github.com/edgexfoundry/edgex-go/internal/security/secretstore" secretstoreConfig "github.com/edgexfoundry/edgex-go/internal/security/secretstore/config" "github.com/edgexfoundry/go-mod-secrets/v3/secrets" @@ -95,20 +99,23 @@ func (p *fileTokenProvider) Run() error { // The tokenConfEnv only uses default settings. tokenConf = tokenConfEnv.mergeWith(tokenConf) + credentialGenerator := secretstore.NewDefaultCredentialGenerator() + + userManager := common.NewUserManager(p.logger, p.secretStoreClient, p.tokenConfig.UserPassMountPoint, "edgex-identity", + privilegedToken, p.tokenConfig.DefaultTokenTTL, p.tokenConfig.DefaultJWTTTL) + for serviceName, serviceConfig := range tokenConf { p.logger.Infof("generating policy/token defaults for service %s", serviceName) servicePolicy := make(map[string]interface{}) - createTokenParameters := make(map[string]interface{}) if serviceConfig.UseDefaults { p.logger.Infof("using policy/token defaults for service %s", serviceName) - servicePolicy = makeDefaultTokenPolicy(serviceName) + servicePolicy = securityCommon.MakeDefaultTokenPolicy(serviceName) defaultPolicyPaths := servicePolicy["path"].(map[string]interface{}) for pathKey, policy := range defaultPolicyPaths { servicePolicy["path"].(map[string]interface{})[pathKey] = policy } - createTokenParameters = makeDefaultTokenParameters(serviceName, p.tokenConfig.DefaultTokenTTL, p.tokenConfig.DefaultTokenTTL) } if serviceConfig.CustomPolicy != nil { @@ -124,37 +131,29 @@ func (p *fileTokenProvider) Run() error { } } - if serviceConfig.CustomTokenParameters != nil { - // Custom token parameters override the defaults - createTokenParameters = mergeMaps(createTokenParameters, serviceConfig.CustomTokenParameters) - } + // Generate a random password - // Set a meta property that consuming serices can use to automatically scope secret queries - createTokenParameters["meta"] = map[string]interface{}{ - "edgex-service-name": serviceName, + randomPassword, err := credentialGenerator.Generate(context.TODO()) + if err != nil { + return err } - // Always create a policy with this name - policyName := "edgex-service-" + serviceName + // Create a user with the random password - policyBytes, err := json.Marshal(servicePolicy) + err = userManager.CreatePasswordUserWithPolicy(serviceName, randomPassword, "edgex-service-", servicePolicy) if err != nil { - p.logger.Errorf("failed encode service policy for %s: %s", serviceName, err.Error()) return err } - if err := p.secretStoreClient.InstallPolicy(privilegedToken, policyName, string(policyBytes)); err != nil { - p.logger.Errorf("failed to install policy %s: %s", policyName, err.Error()) - return err - } + // Immediately log in the user to get a vault token var createTokenResponse interface{} - - if createTokenResponse, err = p.secretStoreClient.CreateToken(privilegedToken, createTokenParameters); err != nil { - p.logger.Errorf("failed to create vault token for service %s: %s", serviceName, err.Error()) + if createTokenResponse, err = p.secretStoreClient.InternalServiceLogin(privilegedToken, p.tokenConfig.UserPassMountPoint, serviceName, randomPassword); err != nil { return err } + // Serialize the vault token to disk + outputTokenDir := filepath.Join(p.tokenConfig.OutputDir, serviceName) outputTokenFilename := filepath.Join(outputTokenDir, p.tokenConfig.OutputFilename) if err := p.fileOpener.MkdirAll(outputTokenDir, os.FileMode(0700)); err != nil { diff --git a/internal/security/fileprovider/provider_test.go b/internal/security/fileprovider/provider_test.go index 7293e994eb..209883b8f5 100644 --- a/internal/security/fileprovider/provider_test.go +++ b/internal/security/fileprovider/provider_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2019-2023 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except // in compliance with the License. You may obtain a copy of the License at @@ -10,7 +10,8 @@ // or implied. See the License for the specific language governing permissions and limitations under // the License. // -// SPDX-License-Identifier: Apache-2.0' +// SPDX-License-Identifier: Apache-2.0 + package fileprovider import ( @@ -75,17 +76,24 @@ func TestMultipleTokensWithNoDefaults(t *testing.T) { expectedService1Policy := "{}" expectedService2Policy := "{}" - expectedService1Parameters := makeMetaServiceName("service1") - expectedService2Parameters := makeMetaServiceName("service2") + mockSecretStoreClient := &mocks.SecretStoreClient{} - mockSecretStoreClient.On("InstallPolicy", "fake-priv-token", "edgex-service-service1", expectedService1Policy). - Return(nil) - mockSecretStoreClient.On("InstallPolicy", "fake-priv-token", "edgex-service-service2", expectedService2Policy). - Return(nil) - mockSecretStoreClient.On("CreateToken", "fake-priv-token", expectedService1Parameters). - Return(createTokenResponse(), nil) - mockSecretStoreClient.On("CreateToken", "fake-priv-token", expectedService2Parameters). - Return(createTokenResponse(), nil) + mockSecretStoreClient.On("InstallPolicy", "fake-priv-token", "edgex-service-service1", expectedService1Policy).Return(nil) + mockSecretStoreClient.On("InstallPolicy", "fake-priv-token", "edgex-service-service2", expectedService2Policy).Return(nil) + + mockSecretStoreClient.On("CreateOrUpdateIdentity", "fake-priv-token", "service1", map[string]string{"name": "service1"}, []string{"edgex-service-service1"}).Return("service1id", nil) + mockSecretStoreClient.On("CreateOrUpdateUser", "fake-priv-token", "", "service1", mock.AnythingOfType("string"), "", []string{}).Return(nil) + mockSecretStoreClient.On("LookupAuthHandle", "fake-priv-token", "").Return(`{"data":{"userpass/":{"accessor","accessorid"}}}`, nil) + mockSecretStoreClient.On("BindUserToIdentity", "fake-priv-token", "service1id", "{\"data\":{\"userpass/\":{\"accessor\",\"accessorid\"}}}", "service1").Return(nil) + mockSecretStoreClient.On("CreateOrUpdateIdentityRole", "fake-priv-token", "service1", "edgex-identity", "{\"name\": \"service1\"}", "").Return(nil) + mockSecretStoreClient.On("InternalServiceLogin", "fake-priv-token", "", "service1", mock.AnythingOfType("string")).Return(createTokenResponse(), nil) + + mockSecretStoreClient.On("CreateOrUpdateIdentity", "fake-priv-token", "service2", map[string]string{"name": "service2"}, []string{"edgex-service-service2"}).Return("service2id", nil) + mockSecretStoreClient.On("CreateOrUpdateUser", "fake-priv-token", "", "service2", mock.AnythingOfType("string"), "", []string{}).Return(nil) + mockSecretStoreClient.On("LookupAuthHandle", "fake-priv-token", "").Return(`{"data":{"userpass/":{"accessor","accessorid"}}}`, nil) + mockSecretStoreClient.On("BindUserToIdentity", "fake-priv-token", "service2id", "{\"data\":{\"userpass/\":{\"accessor\",\"accessorid\"}}}", "service2").Return(nil) + mockSecretStoreClient.On("CreateOrUpdateIdentityRole", "fake-priv-token", "service2", "edgex-identity", "{\"name\": \"service2\"}", "").Return(nil) + mockSecretStoreClient.On("InternalServiceLogin", "fake-priv-token", "", "service2", mock.AnythingOfType("string")).Return(createTokenResponse(), nil) p := NewTokenProvider(mockLogger, mockFileIoPerformer, mockAuthTokenLoader, mockSecretStoreClient) p.SetConfiguration(secretstoreConfig.SecretStoreInfo{}, config.TokenFileProviderInfo{ @@ -124,14 +132,6 @@ func createTokenResponse() map[string]interface{} { return t } -func makeMetaServiceName(serviceName string) map[string]interface{} { - createTokenParameters := make(map[string]interface{}) - meta := make(map[string]interface{}) - meta["edgex-service-name"] = serviceName - createTokenParameters["meta"] = meta - return createTokenParameters -} - func expectedTokenFile() []byte { tokenResponse := createTokenResponse() b := new(bytes.Buffer) @@ -157,12 +157,15 @@ func TestNoDefaultsCustomPolicy(t *testing.T) { mockAuthTokenLoader.On("Load", privilegedTokenPath).Return("fake-priv-token", nil) expectedService1Policy := `{"path":{"secret/non/standard/location/*":{"capabilities":["list","read"]}}}` - expectedService1Parameters := makeMetaServiceName("myservice") + //expectedService1Parameters := makeMetaServiceName("myservice") mockSecretStoreClient := &mocks.SecretStoreClient{} - mockSecretStoreClient.On("InstallPolicy", "fake-priv-token", "edgex-service-myservice", expectedService1Policy). - Return(nil) - mockSecretStoreClient.On("CreateToken", "fake-priv-token", expectedService1Parameters). - Return(createTokenResponse(), nil) + mockSecretStoreClient.On("InstallPolicy", "fake-priv-token", "edgex-service-myservice", expectedService1Policy).Return(nil) + mockSecretStoreClient.On("CreateOrUpdateIdentity", "fake-priv-token", "myservice", map[string]string{"name": "myservice"}, []string{"edgex-service-myservice"}).Return("myserviceid", nil) + mockSecretStoreClient.On("CreateOrUpdateUser", "fake-priv-token", "", "myservice", mock.AnythingOfType("string"), "", []string{}).Return(nil) + mockSecretStoreClient.On("LookupAuthHandle", "fake-priv-token", "").Return(`{"data":{"userpass/":{"accessor","accessorid"}}}`, nil) + mockSecretStoreClient.On("BindUserToIdentity", "fake-priv-token", "myserviceid", "{\"data\":{\"userpass/\":{\"accessor\",\"accessorid\"}}}", "myservice").Return(nil) + mockSecretStoreClient.On("CreateOrUpdateIdentityRole", "fake-priv-token", "myservice", "edgex-identity", "{\"name\": \"myservice\"}", "").Return(nil) + mockSecretStoreClient.On("InternalServiceLogin", "fake-priv-token", "", "myservice", mock.AnythingOfType("string")).Return(createTokenResponse(), nil) p := NewTokenProvider(mockLogger, mockFileIoPerformer, mockAuthTokenLoader, mockSecretStoreClient) p.SetConfiguration(secretstoreConfig.SecretStoreInfo{}, config.TokenFileProviderInfo{ @@ -186,54 +189,6 @@ func TestNoDefaultsCustomPolicy(t *testing.T) { assert.Equal(t, expectedTokenFile(), service1Buffer.Bytes()) } -// TestNoDefaultsCustomTokenParameters -func TestNoDefaultsCustomTokenParameters(t *testing.T) { - // Arrange - mockLogger := logger.MockLogger{} - - mockFileIoPerformer := &fileMock.FileIoPerformer{} - expectedService1Dir := filepath.Join(outputDir, "myservice") - expectedService1File := filepath.Join(expectedService1Dir, outputFilename) - service1Buffer := new(bytes.Buffer) - mockFileIoPerformer.On("MkdirAll", expectedService1Dir, os.FileMode(0700)).Return(nil) - mockFileIoPerformer.On("OpenFileReader", configFile, os.O_RDONLY, os.FileMode(0400)).Return(strings.NewReader(`{"myservice":{"custom_token_parameters":{"key1":"value1"}}}`), nil) - mockFileIoPerformer.On("OpenFileWriter", expectedService1File, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, os.FileMode(0600)).Return(&writeCloserBuffer{service1Buffer}, nil) - - mockAuthTokenLoader := &loaderMock.AuthTokenLoader{} - mockAuthTokenLoader.On("Load", privilegedTokenPath).Return("fake-priv-token", nil) - - expectedService1Policy := "{}" - expectedService1Parameters := make(map[string]interface{}) - expectedService1Parameters["key1"] = "value1" - expectedService1Parameters["meta"] = makeMetaServiceName("myservice")["meta"] - mockSecretStoreClient := &mocks.SecretStoreClient{} - mockSecretStoreClient.On("InstallPolicy", "fake-priv-token", "edgex-service-myservice", expectedService1Policy). - Return(nil) - mockSecretStoreClient.On("CreateToken", "fake-priv-token", expectedService1Parameters). - Return(createTokenResponse(), nil) - - p := NewTokenProvider(mockLogger, mockFileIoPerformer, mockAuthTokenLoader, mockSecretStoreClient) - p.SetConfiguration(secretstoreConfig.SecretStoreInfo{}, config.TokenFileProviderInfo{ - PrivilegedTokenPath: privilegedTokenPath, - ConfigFile: configFile, - OutputDir: outputDir, - OutputFilename: outputFilename, - }) - - // Act - err := p.Run() - - // Assert - // - {OutputDir}/myservice/{OutputFilename} w/proper contents - // - Correct token parameters for myservice - // - All other expectations met - assert.NoError(t, err) - mockFileIoPerformer.AssertExpectations(t) - mockAuthTokenLoader.AssertExpectations(t) - mockSecretStoreClient.AssertExpectations(t) - assert.Equal(t, expectedTokenFile(), service1Buffer.Bytes()) -} - // TestTokenUsingDefaults func TestTokenUsingDefaults(t *testing.T) { // Good cases: @@ -284,12 +239,14 @@ func TestTokenFilePermissions(t *testing.T) { mockAuthTokenLoader := &loaderMock.AuthTokenLoader{} mockAuthTokenLoader.On("Load", privilegedTokenPath).Return("fake-priv-token", nil) - expectedService1Parameters := makeMetaServiceName("myservice") mockSecretStoreClient := &mocks.SecretStoreClient{} - mockSecretStoreClient.On("InstallPolicy", "fake-priv-token", "edgex-service-myservice", "{}"). - Return(nil) - mockSecretStoreClient.On("CreateToken", "fake-priv-token", expectedService1Parameters). - Return(createTokenResponse(), nil) + mockSecretStoreClient.On("InstallPolicy", "fake-priv-token", "edgex-service-myservice", "{}").Return(nil) + mockSecretStoreClient.On("CreateOrUpdateIdentity", "fake-priv-token", "myservice", map[string]string{"name": "myservice"}, []string{"edgex-service-myservice"}).Return("myserviceid", nil) + mockSecretStoreClient.On("CreateOrUpdateUser", "fake-priv-token", "", "myservice", mock.AnythingOfType("string"), "", []string{}).Return(nil) + mockSecretStoreClient.On("LookupAuthHandle", "fake-priv-token", "").Return(`{"data":{"userpass/":{"accessor","accessorid"}}}`, nil) + mockSecretStoreClient.On("BindUserToIdentity", "fake-priv-token", "myserviceid", "{\"data\":{\"userpass/\":{\"accessor\",\"accessorid\"}}}", "myservice").Return(nil) + mockSecretStoreClient.On("CreateOrUpdateIdentityRole", "fake-priv-token", "myservice", "edgex-identity", "{\"name\": \"myservice\"}", "").Return(nil) + mockSecretStoreClient.On("InternalServiceLogin", "fake-priv-token", "", "myservice", mock.AnythingOfType("string")).Return(createTokenResponse(), nil) p := NewTokenProvider(mockLogger, mockFileIoPerformer, mockAuthTokenLoader, mockSecretStoreClient) p.SetConfiguration(secretstoreConfig.SecretStoreInfo{}, config.TokenFileProviderInfo{ @@ -430,6 +387,12 @@ func runTokensWithDefault(serviceName string, additionalKeysEnv string, t *testi policy := map[string]interface{}{ "path": map[string]interface{}{ + "identity/oidc/introspect": map[string]interface{}{ + "capabilities": []string{"create", "update"}, + }, + "identity/oidc/token/" + serviceName: map[string]interface{}{ + "capabilities": []string{"read"}, + }, "secret/edgex/" + serviceName + "/*": map[string]interface{}{ "capabilities": []string{"create", "update", "delete", "list", "read"}, }, @@ -440,19 +403,26 @@ func runTokensWithDefault(serviceName string, additionalKeysEnv string, t *testi } expectedService1Policy, err := json.Marshal(&policy) require.NoError(t, err) - expectedService1Parameters := makeDefaultTokenParameters(serviceName, "1h", "1h") - expectedService1Parameters["meta"] = makeMetaServiceName(serviceName)["meta"] mockSecretStoreClient := &mocks.SecretStoreClient{} - mockSecretStoreClient.On("InstallPolicy", "fake-priv-token", "edgex-service-"+serviceName, string(expectedService1Policy)). - Return(nil) - mockSecretStoreClient.On("CreateToken", "fake-priv-token", expectedService1Parameters, mock.Anything). - Return(createTokenResponse(), nil) + mockSecretStoreClient.On("InstallPolicy", "fake-priv-token", "edgex-service-"+serviceName, string(expectedService1Policy)).Return(nil) + mockSecretStoreClient.On("CreateOrUpdateIdentity", "fake-priv-token", serviceName, map[string]string{"name": serviceName}, []string{"edgex-service-" + serviceName}).Return("myserviceid", nil) + mockSecretStoreClient.On("CreateOrUpdateUser", "fake-priv-token", "", serviceName, mock.AnythingOfType("string"), "1h", []string{}).Return(nil) + mockSecretStoreClient.On("LookupAuthHandle", "fake-priv-token", "").Return(`{"data":{"userpass/":{"accessor","accessorid"}}}`, nil) + mockSecretStoreClient.On("BindUserToIdentity", "fake-priv-token", "myserviceid", "{\"data\":{\"userpass/\":{\"accessor\",\"accessorid\"}}}", serviceName).Return(nil) + mockSecretStoreClient.On("CreateOrUpdateIdentityRole", "fake-priv-token", serviceName, "edgex-identity", "{\"name\": \""+serviceName+"\"}", "").Return(nil) + mockSecretStoreClient.On("InternalServiceLogin", "fake-priv-token", "", serviceName, mock.AnythingOfType("string")).Return(createTokenResponse(), nil) // setup expected things for additional services from env if any for service := range expectedTokenConfigs { policy := map[string]interface{}{ "path": map[string]interface{}{ + "identity/oidc/introspect": map[string]interface{}{ + "capabilities": []string{"create", "update"}, + }, + "identity/oidc/token/" + service: map[string]interface{}{ + "capabilities": []string{"read"}, + }, "secret/edgex/" + service + "/*": map[string]interface{}{ "capabilities": []string{"create", "update", "delete", "list", "read"}, }, @@ -464,14 +434,13 @@ func runTokensWithDefault(serviceName string, additionalKeysEnv string, t *testi expectedServicePolicy, err := json.Marshal(&policy) require.NoError(t, err) - expectedServiceParameters := makeDefaultTokenParameters(service, "1h", "1h") - - expectedServiceParameters["meta"] = makeMetaServiceName(service)["meta"] - - mockSecretStoreClient.On("InstallPolicy", "fake-priv-token", "edgex-service-"+service, string(expectedServicePolicy)). - Return(nil) - mockSecretStoreClient.On("CreateToken", "fake-priv-token", expectedServiceParameters, mock.Anything). - Return(createTokenResponse(), nil) + mockSecretStoreClient.On("InstallPolicy", "fake-priv-token", "edgex-service-"+service, string(expectedServicePolicy)).Return(nil) + mockSecretStoreClient.On("CreateOrUpdateIdentity", "fake-priv-token", service, map[string]string{"name": service}, []string{"edgex-service-" + service}).Return("myserviceid", nil) + mockSecretStoreClient.On("CreateOrUpdateUser", "fake-priv-token", "", service, mock.AnythingOfType("string"), "1h", []string{}).Return(nil) + mockSecretStoreClient.On("LookupAuthHandle", "fake-priv-token", "").Return(`{"data":{"userpass/":{"accessor","accessorid"}}}`, nil) + mockSecretStoreClient.On("BindUserToIdentity", "fake-priv-token", "myserviceid", "{\"data\":{\"userpass/\":{\"accessor\",\"accessorid\"}}}", service).Return(nil) + mockSecretStoreClient.On("CreateOrUpdateIdentityRole", "fake-priv-token", service, "edgex-identity", "{\"name\": \""+service+"\"}", "").Return(nil) + mockSecretStoreClient.On("InternalServiceLogin", "fake-priv-token", "", service, mock.AnythingOfType("string")).Return(createTokenResponse(), nil) } p := NewTokenProvider(mockLogger, mockFileIoPerformer, mockAuthTokenLoader, mockSecretStoreClient) diff --git a/internal/security/fileprovider/tokenconfig.go b/internal/security/fileprovider/tokenconfig.go index 0cd21a93b7..3c65a11454 100644 --- a/internal/security/fileprovider/tokenconfig.go +++ b/internal/security/fileprovider/tokenconfig.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2019-2023 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except // in compliance with the License. You may obtain a copy of the License at @@ -11,7 +11,7 @@ // or implied. See the License for the specific language governing permissions and limitations under // the License. // -// SPDX-License-Identifier: Apache-2.0' +// SPDX-License-Identifier: Apache-2.0 // package fileprovider @@ -67,10 +67,9 @@ type FilePermissions struct { } type ServiceKey struct { - UseDefaults bool `json:"edgex_use_defaults"` - CustomPolicy map[string]interface{} `json:"custom_policy"` // JSON serialization of HCL - CustomTokenParameters map[string]interface{} `json:"custom_token_parameters"` - FilePermissions *FilePermissions `json:"file_permissions,omitempty"` + UseDefaults bool `json:"edgex_use_defaults"` + CustomPolicy map[string]interface{} `json:"custom_policy"` // JSON serialization of HCL + FilePermissions *FilePermissions `json:"file_permissions,omitempty"` } func LoadTokenConfig(fileOpener fileioperformer.FileIoPerformer, path string, tokenConf *TokenConfFile) error { diff --git a/internal/security/fileprovider/tokenconfig_test.go b/internal/security/fileprovider/tokenconfig_test.go index 0e802bfc36..d9007b1039 100644 --- a/internal/security/fileprovider/tokenconfig_test.go +++ b/internal/security/fileprovider/tokenconfig_test.go @@ -55,7 +55,6 @@ func TestLoadTokenConfig(t *testing.T) { var path = aService.CustomPolicy["path"].(map[string]interface{}) assert.Contains(t, path, "secret/non/standard/location/*") // Don't need to go further down the type assertion rabbit hole to prove that this is working - assert.Contains(t, aService.CustomTokenParameters, "custom_option") } func TestLoadTokenConfigError1(t *testing.T) { diff --git a/internal/security/secretstore/constants.go b/internal/security/secretstore/constants.go index c53c96db4d..b14bf74bd4 100644 --- a/internal/security/secretstore/constants.go +++ b/internal/security/secretstore/constants.go @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright 2021 Intel Corporation + * Copyright 2021-2023 Intel Corporation * Copyright 2019 Dell Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except @@ -39,26 +39,45 @@ const ( // per-service tokens and policies // nolint:gosec TokenCreatorPolicy = ` -path "auth/token/create" { - capabilities = ["create", "update", "sudo"] +path "identity/entity/name" { + capabilities = ["list"] +} + +path "identity/entity/name/*" { + capabilities = ["create", "update", "read"] } -path "auth/token/create-orphan" { - capabilities = ["create", "update", "sudo"] +path "identity/entity-alias" { + capabilities = ["create", "update"] } -path "auth/token/create/*" { - capabilities = ["create", "update", "sudo"] +path "identity/oidc/role" { + capabilities = ["list"] } -path "sys/policies/acl/edgex-service-*" -{ +path "identity/oidc/role/*" { + capabilities = ["create", "update"] +} + +path "auth/userpass/users/*" { + capabilities = ["create", "update"] + } + +path "sys/auth" { + capabilities = ["read"] +} + +path "sys/policies/acl/edgex-service-*" { capabilities = ["create", "read", "update", "delete" ] } -path "sys/policies/acl" -{ +path "sys/policies/acl" { capabilities = ["list"] } ` + + // UPAuthMountPoint is where the username/password auth engine is mounted + UPAuthMountPoint = "userpass" + // UserPassAuthEngine is the auth engine name + UserPassAuthEngine = "userpass" ) diff --git a/internal/security/secretstore/init.go b/internal/security/secretstore/init.go index 0406474e18..b3cb95ade0 100644 --- a/internal/security/secretstore/init.go +++ b/internal/security/secretstore/init.go @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright 2022 Intel Corporation + * Copyright 2022-2023 Intel Corporation * Copyright 2019 Dell Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except @@ -147,7 +147,7 @@ func (b *Bootstrap) BootstrapHandler(ctx context.Context, _ *sync.WaitGroup, _ s switch sCode { case http.StatusOK: // Load the init response from disk since we need it to regenerate root token later - if err := loadInitResponse(lc, fileOpener, secretStoreConfig, &initResponse); err != nil { + if err := LoadInitResponse(lc, fileOpener, secretStoreConfig, &initResponse); err != nil { lc.Errorf("unable to load init response: %s", err.Error()) return true } @@ -196,7 +196,7 @@ func (b *Bootstrap) BootstrapHandler(ctx context.Context, _ *sync.WaitGroup, _ s case http.StatusServiceUnavailable: lc.Infof("vault is sealed (status code: %d). Starting unseal phase", sCode) - if err := loadInitResponse(lc, fileOpener, secretStoreConfig, &initResponse); err != nil { + if err := LoadInitResponse(lc, fileOpener, secretStoreConfig, &initResponse); err != nil { lc.Errorf("unable to load init response: %s", err.Error()) return true } @@ -320,6 +320,33 @@ func (b *Bootstrap) BootstrapHandler(ctx context.Context, _ *sync.WaitGroup, _ s } } + // Enable userpass auth engine + upAuthEnabled, err := client.CheckAuthMethodEnabled(rootToken, UPAuthMountPoint, UserPassAuthEngine) + if err != nil { + lc.Errorf("failed to check if %s auth method enabled: %s", UserPassAuthEngine, err.Error()) + return false + } else if !upAuthEnabled { + // Enable userpass engine at /v1/auth/{eng.path} path (/v1 prefix supplied by Vault) + lc.Infof("Enabling userpass authentication for the first time...") + if err := client.EnablePasswordAuth(rootToken, UPAuthMountPoint); err != nil { + lc.Errorf("failed to enable userpass secrets engine: %s", err.Error()) + return false + } + lc.Infof("Userpass authentication engine enabled at path %s", UPAuthMountPoint) + } + + // Create a key for issuing JWTs + keyExists, err := client.CheckIdentityKeyExists(rootToken, "edgex-identity") + if err != nil { + lc.Errorf("failed to check for JWT issuing key: %s", err.Error()) + return false + } else if !keyExists { + if err := client.CreateNamedIdentityKey(rootToken, "edgex-identity", "ES384"); err != nil { + lc.Errorf("failed to create JWT issuing key: %s", err.Error()) + return false + } + } + //Step 4: Launch token handler tokenProvider := NewTokenProvider(ctx, lc, NewDefaultExecRunner()) if secretStoreConfig.TokenProvider != "" { @@ -368,7 +395,7 @@ func (b *Bootstrap) BootstrapHandler(ctx context.Context, _ *sync.WaitGroup, _ s redisCredentials, err := getCredential("security-bootstrapper-redis", secretStore, redisSecretName) if err != nil { if err != errNotFound { - lc.Error("failed to determine if Redis credentials already exist or not: %w", err) + lc.Errorf("failed to determine if Redis credentials already exist or not: %s", err.Error()) return false } @@ -429,7 +456,7 @@ func (b *Bootstrap) BootstrapHandler(ctx context.Context, _ *sync.WaitGroup, _ s msgBusCredentials, err = getCredential(internal.BootstrapMessageBusServiceKey, secretStore, messagebusSecretName) if err != nil { if err != errNotFound { - lc.Errorf("failed to determine if %s credentials already exist or not: %w", configuration.SecureMessageBus.Type, err) + lc.Errorf("failed to determine if %s credentials already exist or not: %s", configuration.SecureMessageBus.Type, err.Error()) return false } @@ -726,7 +753,7 @@ func storeCredential(lc logger.LoggingClient, credBootstrapStem string, cred Cre return err } -func loadInitResponse( +func LoadInitResponse( lc logger.LoggingClient, fileOpener fileioperformer.FileIoPerformer, secretConfig config.SecretStoreInfo, diff --git a/internal/security/secretstore/init_test.go b/internal/security/secretstore/init_test.go index b17d32714d..ca4a21de6d 100644 --- a/internal/security/secretstore/init_test.go +++ b/internal/security/secretstore/init_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Intel Corporation +// Copyright (c) 2021-2023 Intel Corporation // // SPDX-License-Identifier: Apache-2.0 // @@ -49,7 +49,7 @@ func TestLoadInitResponse(t *testing.T) { initResponse := types.InitResponse{} // Act - err := loadInitResponse(mockLogger, fileOpener, secretConfig, &initResponse) + err := LoadInitResponse(mockLogger, fileOpener, secretConfig, &initResponse) // Assert assert.NoError(t, err) diff --git a/internal/security/spiffetokenprovider/config/config.go b/internal/security/spiffetokenprovider/config/config.go index 69bbca657e..449a11d97f 100644 --- a/internal/security/spiffetokenprovider/config/config.go +++ b/internal/security/spiffetokenprovider/config/config.go @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright 2022 Intel Corporation + * Copyright 2022-2023 Intel Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at @@ -16,6 +16,7 @@ package config import ( + fileProviderConfig "github.com/edgexfoundry/edgex-go/internal/security/fileprovider/config" bootstrapConfig "github.com/edgexfoundry/go-mod-bootstrap/v3/config" ) @@ -28,13 +29,14 @@ type SpiffeInfo struct { } type ConfigurationStruct struct { - Writable WritableInfo - MessageBus bootstrapConfig.MessageBusInfo - Clients map[string]bootstrapConfig.ClientInfo - Database bootstrapConfig.Database - Registry bootstrapConfig.RegistryInfo - Service bootstrapConfig.ServiceInfo - Spiffe SpiffeInfo + Writable WritableInfo + MessageBus bootstrapConfig.MessageBusInfo + Clients map[string]bootstrapConfig.ClientInfo + Database bootstrapConfig.Database + Registry bootstrapConfig.RegistryInfo + Service bootstrapConfig.ServiceInfo + TokenConfig fileProviderConfig.TokenFileProviderInfo + Spiffe SpiffeInfo } type WritableInfo struct { diff --git a/internal/security/spiffetokenprovider/init.go b/internal/security/spiffetokenprovider/init.go index 7ba6224474..d734c90d66 100644 --- a/internal/security/spiffetokenprovider/init.go +++ b/internal/security/spiffetokenprovider/init.go @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright 2022 Intel Corporation + * Copyright 2022-2023 Intel Corporation * Copyright 2019 Dell Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except @@ -265,7 +265,7 @@ func (b *Bootstrap) BootstrapHandler(ctx context.Context, _ *sync.WaitGroup, _ s w.WriteHeader(http.StatusInternalServerError) return } - vaultTokenResponse, err := makeToken(serviceName, privilegedToken, secretStoreClient, lc) + vaultTokenResponse, err := makeToken(serviceName, privilegedToken, configuration.TokenConfig, secretStoreClient, lc) if err != nil { lc.Errorf("failed create secret store token: %v", err) w.WriteHeader(http.StatusInternalServerError) diff --git a/internal/security/spiffetokenprovider/maketoken.go b/internal/security/spiffetokenprovider/maketoken.go index 37e9fca094..c716bc979f 100644 --- a/internal/security/spiffetokenprovider/maketoken.go +++ b/internal/security/spiffetokenprovider/maketoken.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2022 Intel Corporation +// Copyright (c) 2022-2023 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except // in compliance with the License. You may obtain a copy of the License at @@ -17,94 +17,53 @@ package spiffetokenprovider import ( - "encoding/json" - "fmt" + "context" + "github.com/edgexfoundry/edgex-go/internal/security/common" + securityCommon "github.com/edgexfoundry/edgex-go/internal/security/common" + fileProviderConfig "github.com/edgexfoundry/edgex-go/internal/security/fileprovider/config" + "github.com/edgexfoundry/edgex-go/internal/security/secretstore" "github.com/edgexfoundry/go-mod-core-contracts/v3/clients/logger" "github.com/edgexfoundry/go-mod-secrets/v3/secrets" ) -const ( - DefaultTokenTTL = "1h" -) - -func makeDefaultTokenPolicy(serviceName string) map[string]interface{} { - // protected path for secret/ - protectedPath := "secret/edgex/" + serviceName + "/*" - capabilities := []string{"create", "update", "delete", "list", "read"} - acl := map[string]interface{}{"capabilities": capabilities} - // path for consul tokens - registryCredsPath := "consul/creds/" + serviceName - registryCredsCapabilities := []string{"read"} - registryCredsACL := map[string]interface{}{"capabilities": registryCredsCapabilities} - pathObject := map[string]interface{}{ - protectedPath: acl, - registryCredsPath: registryCredsACL, - } - retval := map[string]interface{}{"path": pathObject} - return retval - - /* - { - "path": { - "secret/edgex/service-name/*": { - "capabilities": [ "create", "update", "delete", "list", "read" ] - }, - "consul/creds/service-name": { - "capabilities": [ "read" ] - } - } - } - */ -} - -func makeDefaultTokenParameters(serviceName string, defaultTTL string, defaultPeriod string) map[string]interface{} { - return map[string]interface{}{ - "display_name": serviceName, - "no_parent": true, - "ttl": defaultTTL, - "period": defaultPeriod, - "policies": []string{"edgex-service-" + serviceName}, - } -} - func makeToken(serviceName string, privilegedToken string, + tokenConfig fileProviderConfig.TokenFileProviderInfo, secretStoreClient secrets.SecretStoreClient, lc logger.LoggingClient) (interface{}, error) { lc.Infof("generating policy/token defaults for service %s", serviceName) lc.Infof("using policy/token defaults for service %s", serviceName) - servicePolicy := makeDefaultTokenPolicy(serviceName) + servicePolicy := securityCommon.MakeDefaultTokenPolicy(serviceName) defaultPolicyPaths := servicePolicy["path"].(map[string]interface{}) for pathKey, policy := range defaultPolicyPaths { servicePolicy["path"].(map[string]interface{})[pathKey] = policy } - createTokenParameters := makeDefaultTokenParameters(serviceName, DefaultTokenTTL, DefaultTokenTTL) - // Set a meta property that consuming serices can use to automatically scope secret queries - createTokenParameters["meta"] = map[string]interface{}{ - "edgex-service-name": serviceName, - } + credentialGenerator := secretstore.NewDefaultCredentialGenerator() - // Always create a policy with this name - policyName := "edgex-service-" + serviceName + userManager := common.NewUserManager(lc, secretStoreClient, tokenConfig.UserPassMountPoint, "edgex-identity", + privilegedToken, tokenConfig.DefaultTokenTTL, tokenConfig.DefaultJWTTTL) - policyBytes, err := json.Marshal(servicePolicy) + // Generate a random password + + randomPassword, err := credentialGenerator.Generate(context.TODO()) if err != nil { - lc.Error(fmt.Sprintf("failed encode service policy for %s: %s", serviceName, err.Error())) return nil, err } - if err := secretStoreClient.InstallPolicy(privilegedToken, policyName, string(policyBytes)); err != nil { - lc.Error(fmt.Sprintf("failed to install policy %s: %s", policyName, err.Error())) + // Create a user with the random password + + err = userManager.CreatePasswordUserWithPolicy(serviceName, randomPassword, "edgex-service-", servicePolicy) + if err != nil { return nil, err } - var createTokenResponse interface{} + // Immediately log in the user to get a vault token - if createTokenResponse, err = secretStoreClient.CreateToken(privilegedToken, createTokenParameters); err != nil { - lc.Error(fmt.Sprintf("failed to create vault token for service %s: %s", serviceName, err.Error())) + var createTokenResponse interface{} + if createTokenResponse, err = secretStoreClient.InternalServiceLogin(privilegedToken, tokenConfig.UserPassMountPoint, serviceName, randomPassword); err != nil { return nil, err }