diff options
author | Kiran Kamineni <kiran.k.kamineni@intel.com> | 2018-03-23 14:14:10 -0700 |
---|---|---|
committer | Kiran Kamineni <kiran.k.kamineni@intel.com> | 2018-03-23 14:17:47 -0700 |
commit | 937a0cdf641aa66ba8cf413f684d67dc3a9fd90d (patch) | |
tree | 9bb2638b18ca96596d568f384bf221296551e660 | |
parent | 7cdae555806b7fb642460845961295992c6f7f34 (diff) |
Adding system test coverage support
Added support for running system level code coverage tasks
Updated sms.go to allow graceful shutdown when it
gets a SIGINT. Useful for gather coverage information.
Issue-ID: AAF-192
Change-Id: Ife4a485e7926fd59948bf90fac4b2d4ea9de0332
Signed-off-by: Kiran Kamineni <kiran.k.kamineni@intel.com>
-rw-r--r-- | sms-service/doc/coverage.html | 1182 | ||||
-rw-r--r-- | sms-service/src/sms/Gopkg.lock | 22 | ||||
-rw-r--r-- | sms-service/src/sms/Gopkg.toml | 2 | ||||
-rw-r--r-- | sms-service/src/sms/coverage.md | 41 | ||||
-rw-r--r-- | sms-service/src/sms/sms.go | 19 | ||||
-rw-r--r-- | sms-service/src/sms/sms_test.go | 31 |
6 files changed, 1284 insertions, 13 deletions
diff --git a/sms-service/doc/coverage.html b/sms-service/doc/coverage.html new file mode 100644 index 0000000..d03ddde --- /dev/null +++ b/sms-service/doc/coverage.html @@ -0,0 +1,1182 @@ + +<!DOCTYPE html> +<html> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> + <style> + body { + background: black; + color: rgb(80, 80, 80); + } + body, pre, #legend span { + font-family: Menlo, monospace; + font-weight: bold; + } + #topbar { + background: black; + position: fixed; + top: 0; left: 0; right: 0; + height: 42px; + border-bottom: 1px solid rgb(80, 80, 80); + } + #content { + margin-top: 50px; + } + #nav, #legend { + float: left; + margin-left: 10px; + } + #legend { + margin-top: 12px; + } + #nav { + margin-top: 10px; + } + #legend span { + margin: 0 5px; + } + .cov0 { color: rgb(192, 0, 0) } +.cov1 { color: rgb(128, 128, 128) } +.cov2 { color: rgb(116, 140, 131) } +.cov3 { color: rgb(104, 152, 134) } +.cov4 { color: rgb(92, 164, 137) } +.cov5 { color: rgb(80, 176, 140) } +.cov6 { color: rgb(68, 188, 143) } +.cov7 { color: rgb(56, 200, 146) } +.cov8 { color: rgb(44, 212, 149) } +.cov9 { color: rgb(32, 224, 152) } +.cov10 { color: rgb(20, 236, 155) } + + </style> + </head> + <body> + <div id="topbar"> + <div id="nav"> + <select id="files"> + + <option value="file0">sms/auth/auth.go (17.6%)</option> + + <option value="file1">sms/backend/backend.go (66.7%)</option> + + <option value="file2">sms/backend/vault.go (60.5%)</option> + + <option value="file3">sms/config/config.go (90.9%)</option> + + <option value="file4">sms/handler/handler.go (55.1%)</option> + + <option value="file5">sms/log/logger.go (31.2%)</option> + + <option value="file6">sms/sms.go (82.6%)</option> + + </select> + </div> + <div id="legend"> + <span>not tracked</span> + + <span class="cov0">no coverage</span> + <span class="cov1">low coverage</span> + <span class="cov2">*</span> + <span class="cov3">*</span> + <span class="cov4">*</span> + <span class="cov5">*</span> + <span class="cov6">*</span> + <span class="cov7">*</span> + <span class="cov8">*</span> + <span class="cov9">*</span> + <span class="cov10">high coverage</span> + + </div> + </div> + <div id="content"> + + <pre class="file" id="file0" style="display: none">/* + * Copyright 2018 Intel Corporation, Inc + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package auth + +import ( + "bytes" + "crypto/tls" + "crypto/x509" + "encoding/base64" + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/packet" + "io/ioutil" + + smslogger "sms/log" +) + +var tlsConfig *tls.Config + +// GetTLSConfig initializes a tlsConfig using the CA's certificate +// This config is then used to enable the server for mutual TLS +func GetTLSConfig(caCertFile string) (*tls.Config, error) <span class="cov10" title="3">{ + // Initialize tlsConfig once + if tlsConfig == nil </span><span class="cov10" title="3">{ + caCert, err := ioutil.ReadFile(caCertFile) + + if err != nil </span><span class="cov1" title="1">{ + return nil, err + }</span> + + <span class="cov6" title="2">caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(caCert) + + tlsConfig = &tls.Config{ + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: caCertPool, + MinVersion: tls.VersionTLS12, + } + tlsConfig.BuildNameToCertificate()</span> + } + <span class="cov6" title="2">return tlsConfig, nil</span> +} + +// GeneratePGPKeyPair produces a PGP key pair and returns +// two things: +// A base64 encoded form of the public part of the entity +// A base64 encoded form of the private key +func GeneratePGPKeyPair() (string, string, error) <span class="cov0" title="0">{ + var entity *openpgp.Entity + entity, err := openpgp.NewEntity("aaf.sms.init", "PGP Key for unsealing", "", nil) + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + return "", "", err + }</span> + + // Sign the identity in the entity + <span class="cov0" title="0">for _, id := range entity.Identities </span><span class="cov0" title="0">{ + err = id.SelfSignature.SignUserId(id.UserId.Id, entity.PrimaryKey, entity.PrivateKey, nil) + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + return "", "", err + }</span> + } + + // Sign the subkey in the entity + <span class="cov0" title="0">for _, subkey := range entity.Subkeys </span><span class="cov0" title="0">{ + err := subkey.Sig.SignKey(subkey.PublicKey, entity.PrivateKey, nil) + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + return "", "", err + }</span> + } + + <span class="cov0" title="0">buffer := new(bytes.Buffer) + entity.Serialize(buffer) + pbkey := base64.StdEncoding.EncodeToString(buffer.Bytes()) + + buffer.Reset() + entity.SerializePrivate(buffer, nil) + prkey := base64.StdEncoding.EncodeToString(buffer.Bytes()) + + return pbkey, prkey, nil</span> +} + +// DecryptPGPBytes decrypts a PGP encoded input string and returns +// a base64 representation of the decoded string +func DecryptPGPBytes(data string, prKey string) (string, error) <span class="cov0" title="0">{ + // Convert private key to bytes from base64 + prKeyBytes, err := base64.StdEncoding.DecodeString(prKey) + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError("Error Decoding base64 private key: " + err.Error()) + return "", err + }</span> + + <span class="cov0" title="0">dataBytes, err := base64.StdEncoding.DecodeString(data) + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError("Error Decoding base64 data: " + err.Error()) + return "", err + }</span> + + <span class="cov0" title="0">prEntity, err := openpgp.ReadEntity(packet.NewReader(bytes.NewBuffer(prKeyBytes))) + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError("Error reading entity from PGP key: " + err.Error()) + return "", err + }</span> + + <span class="cov0" title="0">prEntityList := &openpgp.EntityList{prEntity} + message, err := openpgp.ReadMessage(bytes.NewBuffer(dataBytes), prEntityList, nil, nil) + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError("Error Decrypting message: " + err.Error()) + return "", err + }</span> + + <span class="cov0" title="0">var retBuf bytes.Buffer + retBuf.ReadFrom(message.UnverifiedBody) + + return retBuf.String(), nil</span> +} +</pre> + + <pre class="file" id="file1" style="display: none">/* + * Copyright 2018 Intel Corporation, Inc + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package backend + +import ( + smsconfig "sms/config" + smslogger "sms/log" +) + +// SecretDomain is where Secrets are stored. +// A single domain can have any number of secrets +type SecretDomain struct { + UUID string `json:"uuid"` + Name string `json:"name"` +} + +// Secret is the struct that defines the structure of a secret +// It consists of a name and map containing key value pairs +type Secret struct { + Name string `json:"name"` + Values map[string]interface{} `json:"values"` +} + +// SecretBackend interface that will be implemented for various secret backends +type SecretBackend interface { + Init() error + GetStatus() (bool, error) + Unseal(shard string) error + + GetSecret(dom string, sec string) (Secret, error) + ListSecret(dom string) ([]string, error) + + CreateSecretDomain(name string) (SecretDomain, error) + CreateSecret(dom string, sec Secret) error + + DeleteSecretDomain(name string) error + DeleteSecret(dom string, name string) error +} + +// InitSecretBackend returns an interface implementation +func InitSecretBackend() (SecretBackend, error) <span class="cov10" title="2">{ + backendImpl := &Vault{ + vaultAddress: smsconfig.SMSConfig.VaultAddress, + vaultToken: smsconfig.SMSConfig.VaultToken, + } + + err := backendImpl.Init() + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + return nil, err + }</span> + + <span class="cov10" title="2">return backendImpl, nil</span> +} + +// LoginBackend Interface that will be implemented for various login backends +type LoginBackend interface { +} +</pre> + + <pre class="file" id="file2" style="display: none">/* + * Copyright 2018 Intel Corporation, Inc + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package backend + +import ( + uuid "github.com/hashicorp/go-uuid" + vaultapi "github.com/hashicorp/vault/api" + smsauth "sms/auth" + smslogger "sms/log" + + "errors" + "fmt" + "strings" + "sync" + "time" +) + +// Vault is the main Struct used in Backend to initialize the struct +type Vault struct { + sync.Mutex + engineType string + initRoleDone bool + policyName string + roleID string + secretID string + vaultAddress string + vaultClient *vaultapi.Client + vaultMount string + vaultTempTokenTTL time.Time + vaultToken string + unsealShards []string + rootToken string + pgpPub string + pgpPr string +} + +// Init will initialize the vault connection +// It will also create the initial policy if it does not exist +// TODO: Check to see if we need to wait for vault to be running +func (v *Vault) Init() error <span class="cov4" title="3">{ + vaultCFG := vaultapi.DefaultConfig() + vaultCFG.Address = v.vaultAddress + client, err := vaultapi.NewClient(vaultCFG) + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + return errors.New("Unable to create new vault client") + }</span> + + <span class="cov4" title="3">v.engineType = "kv" + v.initRoleDone = false + v.policyName = "smsvaultpolicy" + v.vaultClient = client + v.vaultMount = "sms" + + err = v.initRole() + if err != nil </span><span class="cov2" title="2">{ + smslogger.WriteError(err.Error()) + smslogger.WriteInfo("InitRole will try again later") + }</span> + + <span class="cov4" title="3">return nil</span> +} + +// GetStatus returns the current seal status of vault +func (v *Vault) GetStatus() (bool, error) <span class="cov4" title="3">{ + sys := v.vaultClient.Sys() + sealStatus, err := sys.SealStatus() + if err != nil </span><span class="cov1" title="1">{ + smslogger.WriteError(err.Error()) + return false, errors.New("Error getting status") + }</span> + + <span class="cov2" title="2">return sealStatus.Sealed, nil</span> +} + +// Unseal is a passthrough API that allows any +// unseal or initialization processes for the backend +func (v *Vault) Unseal(shard string) error <span class="cov0" title="0">{ + sys := v.vaultClient.Sys() + _, err := sys.Unseal(shard) + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + return errors.New("Unable to execute unseal operation with specified shard") + }</span> + + <span class="cov0" title="0">return nil</span> +} + +// GetSecret returns a secret mounted on a particular domain name +// The secret itself is referenced via its name which translates to +// a mount path in vault +func (v *Vault) GetSecret(dom string, name string) (Secret, error) <span class="cov6" title="6">{ + err := v.checkToken() + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + return Secret{}, errors.New("Token check failed") + }</span> + + <span class="cov6" title="6">dom = v.vaultMount + "/" + dom + + sec, err := v.vaultClient.Logical().Read(dom + "/" + name) + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + return Secret{}, errors.New("Unable to read Secret at provided path") + }</span> + + // sec and err are nil in the case where a path does not exist + <span class="cov6" title="6">if sec == nil </span><span class="cov0" title="0">{ + smslogger.WriteWarn("Vault read was empty. Invalid Path") + return Secret{}, errors.New("Secret not found at the provided path") + }</span> + + <span class="cov6" title="6">return Secret{Name: name, Values: sec.Data}, nil</span> +} + +// ListSecret returns a list of secret names on a particular domain +// The values of the secret are not returned +func (v *Vault) ListSecret(dom string) ([]string, error) <span class="cov2" title="2">{ + err := v.checkToken() + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + return nil, errors.New("Token check failed") + }</span> + + <span class="cov2" title="2">dom = v.vaultMount + "/" + dom + + sec, err := v.vaultClient.Logical().List(dom) + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + return nil, errors.New("Unable to read Secret at provided path") + }</span> + + // sec and err are nil in the case where a path does not exist + <span class="cov2" title="2">if sec == nil </span><span class="cov0" title="0">{ + smslogger.WriteWarn("Vaultclient returned empty data") + return nil, errors.New("Secret not found at the provided path") + }</span> + + <span class="cov2" title="2">val, ok := sec.Data["keys"].([]interface{}) + if !ok </span><span class="cov0" title="0">{ + smslogger.WriteError("Secret not found at the provided path") + return nil, errors.New("Secret not found at the provided path") + }</span> + + <span class="cov2" title="2">retval := make([]string, len(val)) + for i, v := range val </span><span class="cov6" title="6">{ + retval[i] = fmt.Sprint(v) + }</span> + + <span class="cov2" title="2">return retval, nil</span> +} + +// CreateSecretDomain mounts the kv backend on a path with the given name +func (v *Vault) CreateSecretDomain(name string) (SecretDomain, error) <span class="cov2" title="2">{ + // Check if token is still valid + err := v.checkToken() + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + return SecretDomain{}, errors.New("Token Check failed") + }</span> + + <span class="cov2" title="2">name = strings.TrimSpace(name) + mountPath := v.vaultMount + "/" + name + mountInput := &vaultapi.MountInput{ + Type: v.engineType, + Description: "Mount point for domain: " + name, + Local: false, + SealWrap: false, + Config: vaultapi.MountConfigInput{}, + } + + err = v.vaultClient.Sys().Mount(mountPath, mountInput) + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + return SecretDomain{}, errors.New("Unable to create Secret Domain") + }</span> + + <span class="cov2" title="2">uuid, _ := uuid.GenerateUUID() + return SecretDomain{uuid, name}, nil</span> +} + +// CreateSecret creates a secret mounted on a particular domain name +// The secret itself is mounted on a path specified by name +func (v *Vault) CreateSecret(dom string, sec Secret) error <span class="cov6" title="6">{ + err := v.checkToken() + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + return errors.New("Token check failed") + }</span> + + <span class="cov6" title="6">dom = v.vaultMount + "/" + dom + + // Vault return is empty on successful write + // TODO: Check if values is not empty + _, err = v.vaultClient.Logical().Write(dom+"/"+sec.Name, sec.Values) + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + return errors.New("Unable to create Secret at provided path") + }</span> + + <span class="cov6" title="6">return nil</span> +} + +// DeleteSecretDomain deletes a secret domain which translates to +// an unmount operation on the given path in Vault +func (v *Vault) DeleteSecretDomain(name string) error <span class="cov2" title="2">{ + err := v.checkToken() + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + return errors.New("Token Check Failed") + }</span> + + <span class="cov2" title="2">name = strings.TrimSpace(name) + mountPath := v.vaultMount + "/" + name + + err = v.vaultClient.Sys().Unmount(mountPath) + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + return errors.New("Unable to delete domain specified") + }</span> + + <span class="cov2" title="2">return nil</span> +} + +// DeleteSecret deletes a secret mounted on the path provided +func (v *Vault) DeleteSecret(dom string, name string) error <span class="cov6" title="6">{ + err := v.checkToken() + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + return errors.New("Token check failed") + }</span> + + <span class="cov6" title="6">dom = v.vaultMount + "/" + dom + + // Vault return is empty on successful delete + _, err = v.vaultClient.Logical().Delete(dom + "/" + name) + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + return errors.New("Unable to delete Secret at provided path") + }</span> + + <span class="cov6" title="6">return nil</span> +} + +// initRole is called only once during the service bring up +func (v *Vault) initRole() error <span class="cov4" title="3">{ + // Use the root token once here + v.vaultClient.SetToken(v.vaultToken) + defer v.vaultClient.ClearToken() + + rules := `path "sms/*" { capabilities = ["create", "read", "update", "delete", "list"] } + path "sys/mounts/sms*" { capabilities = ["update","delete","create"] }` + err := v.vaultClient.Sys().PutPolicy(v.policyName, rules) + if err != nil </span><span class="cov2" title="2">{ + smslogger.WriteError(err.Error()) + return errors.New("Unable to create policy for approle creation") + }</span> + + <span class="cov1" title="1">rName := v.vaultMount + "-role" + data := map[string]interface{}{ + "token_ttl": "60m", + "policies": [2]string{"default", v.policyName}, + } + + //Check if applrole is mounted + authMounts, err := v.vaultClient.Sys().ListAuth() + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + return errors.New("Unable to get mounted auth backends") + }</span> + + <span class="cov1" title="1">approleMounted := false + for k, v := range authMounts </span><span class="cov1" title="1">{ + if v.Type == "approle" && k == "approle/" </span><span class="cov1" title="1">{ + approleMounted = true + break</span> + } + } + + // Mount approle in case its not already mounted + <span class="cov1" title="1">if !approleMounted </span><span class="cov0" title="0">{ + v.vaultClient.Sys().EnableAuth("approle", "approle", "") + }</span> + + // Create a role-id + <span class="cov1" title="1">v.vaultClient.Logical().Write("auth/approle/role/"+rName, data) + sec, err := v.vaultClient.Logical().Read("auth/approle/role/" + rName + "/role-id") + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + return errors.New("Unable to create role ID for approle") + }</span> + <span class="cov1" title="1">v.roleID = sec.Data["role_id"].(string) + + // Create a secret-id to go with it + sec, err = v.vaultClient.Logical().Write("auth/approle/role/"+rName+"/secret-id", + map[string]interface{}{}) + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + return errors.New("Unable to create secret ID for role") + }</span> + + <span class="cov1" title="1">v.secretID = sec.Data["secret_id"].(string) + v.initRoleDone = true + return nil</span> +} + +// Function checkToken() gets called multiple times to create +// temporary tokens +func (v *Vault) checkToken() error <span class="cov10" title="24">{ + v.Lock() + defer v.Unlock() + + // Init Role if it is not yet done + // Role needs to be created before token can be created + if v.initRoleDone == false </span><span class="cov0" title="0">{ + err := v.initRole() + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + return errors.New("Unable to initRole in checkToken") + }</span> + } + + // Return immediately if token still has life + <span class="cov10" title="24">if v.vaultClient.Token() != "" && + time.Since(v.vaultTempTokenTTL) < time.Minute*50 </span><span class="cov9" title="23">{ + return nil + }</span> + + // Create a temporary token using our roleID and secretID + <span class="cov1" title="1">out, err := v.vaultClient.Logical().Write("auth/approle/login", + map[string]interface{}{"role_id": v.roleID, "secret_id": v.secretID}) + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + return errors.New("Unable to create Temporary Token for Role") + }</span> + + <span class="cov1" title="1">tok, err := out.TokenID() + + v.vaultTempTokenTTL = time.Now() + v.vaultClient.SetToken(tok) + return nil</span> +} + +// vaultInit() is used to initialize the vault in cases where it is not +// initialized. This happens once during intial bring up. +func (v *Vault) initializeVault() error <span class="cov0" title="0">{ + initReq := &vaultapi.InitRequest{ + SecretShares: 5, + SecretThreshold: 3, + } + + pbkey, prkey, err := smsauth.GeneratePGPKeyPair() + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError("Error Generating PGP Keys. Vault Init will not use encryption!") + }</span><span class="cov0" title="0"> else { + initReq.PGPKeys = []string{pbkey, pbkey, pbkey, pbkey, pbkey} + initReq.RootTokenPGPKey = pbkey + v.pgpPub = pbkey + v.pgpPr = prkey + }</span> + + <span class="cov0" title="0">resp, err := v.vaultClient.Sys().Init(initReq) + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + return errors.New("FATAL: Unable to initialize Vault") + }</span> + + <span class="cov0" title="0">if resp != nil </span><span class="cov0" title="0">{ + v.unsealShards = resp.KeysB64 + v.rootToken = resp.RootToken + return nil + }</span> + + <span class="cov0" title="0">return errors.New("FATAL: Init response was empty")</span> +} +</pre> + + <pre class="file" id="file3" style="display: none">/* + * Copyright 2018 Intel Corporation, Inc + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package config + +import ( + "encoding/json" + "os" +) + +// SMSConfiguration loads up all the values that are used to configure +// backend implementations +// TODO: Review these and see if they can be created/discovered dynamically +type SMSConfiguration struct { + CAFile string `json:"cafile"` + ServerCert string `json:"servercert"` + ServerKey string `json:"serverkey"` + + VaultAddress string `json:"vaultaddress"` + VaultToken string `json:"vaulttoken"` +} + +// SMSConfig is the structure that stores the configuration +var SMSConfig *SMSConfiguration + +// ReadConfigFile reads the specified smsConfig file to setup some env variables +func ReadConfigFile(file string) (*SMSConfiguration, error) <span class="cov10" title="3">{ + if SMSConfig == nil </span><span class="cov10" title="3">{ + f, err := os.Open(file) + if err != nil </span><span class="cov1" title="1">{ + return nil, err + }</span> + <span class="cov6" title="2">defer f.Close() + + SMSConfig = &SMSConfiguration{} + decoder := json.NewDecoder(f) + err = decoder.Decode(SMSConfig) + if err != nil </span><span class="cov0" title="0">{ + return nil, err + }</span> + } + + <span class="cov6" title="2">return SMSConfig, nil</span> +} +</pre> + + <pre class="file" id="file4" style="display: none">/* + * Copyright 2018 Intel Corporation, Inc + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package handler + +import ( + "encoding/json" + "github.com/gorilla/mux" + "net/http" + + smsbackend "sms/backend" + smslogger "sms/log" +) + +// handler stores two interface implementations that implement +// the backend functionality +type handler struct { + secretBackend smsbackend.SecretBackend + loginBackend smsbackend.LoginBackend +} + +// createSecretDomainHandler creates a secret domain with a name provided +func (h handler) createSecretDomainHandler(w http.ResponseWriter, r *http.Request) <span class="cov6" title="3">{ + var d smsbackend.SecretDomain + + err := json.NewDecoder(r.Body).Decode(&d) + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + http.Error(w, err.Error(), http.StatusBadRequest) + return + }</span> + + <span class="cov6" title="3">dom, err := h.secretBackend.CreateSecretDomain(d.Name) + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + }</span> + + <span class="cov6" title="3">jdata, err := json.Marshal(dom) + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + }</span> + + <span class="cov6" title="3">w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + w.Write(jdata)</span> +} + +// deleteSecretDomainHandler deletes a secret domain with the name provided +func (h handler) deleteSecretDomainHandler(w http.ResponseWriter, r *http.Request) <span class="cov6" title="3">{ + vars := mux.Vars(r) + domName := vars["domName"] + + err := h.secretBackend.DeleteSecretDomain(domName) + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + }</span> + + <span class="cov6" title="3">w.WriteHeader(http.StatusNoContent)</span> +} + +// createSecretHandler handles creation of secrets on a given domain name +func (h handler) createSecretHandler(w http.ResponseWriter, r *http.Request) <span class="cov10" title="7">{ + // Get domain name from URL + vars := mux.Vars(r) + domName := vars["domName"] + + // Get secrets to be stored from body + var b smsbackend.Secret + err := json.NewDecoder(r.Body).Decode(&b) + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + http.Error(w, err.Error(), http.StatusBadRequest) + return + }</span> + + <span class="cov10" title="7">err = h.secretBackend.CreateSecret(domName, b) + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + }</span> + + <span class="cov10" title="7">w.WriteHeader(http.StatusCreated)</span> +} + +// getSecretHandler handles reading a secret by given domain name and secret name +func (h handler) getSecretHandler(w http.ResponseWriter, r *http.Request) <span class="cov10" title="7">{ + vars := mux.Vars(r) + domName := vars["domName"] + secName := vars["secretName"] + + sec, err := h.secretBackend.GetSecret(domName, secName) + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + }</span> + + <span class="cov10" title="7">jdata, err := json.Marshal(sec) + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + }</span> + + <span class="cov10" title="7">w.Header().Set("Content-Type", "application/json") + w.Write(jdata)</span> +} + +// listSecretHandler handles listing all secrets under a particular domain name +func (h handler) listSecretHandler(w http.ResponseWriter, r *http.Request) <span class="cov6" title="3">{ + vars := mux.Vars(r) + domName := vars["domName"] + + secList, err := h.secretBackend.ListSecret(domName) + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + }</span> + + // Creating an anonymous struct to store the returned list of data + <span class="cov6" title="3">var retStruct = struct { + SecretNames []string `json:"secretnames"` + }{ + secList, + } + + jdata, err := json.Marshal(retStruct) + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + }</span> + + <span class="cov6" title="3">w.Header().Set("Content-Type", "application/json") + w.Write(jdata)</span> +} + +// deleteSecretHandler handles deleting a secret by given domain name and secret name +func (h handler) deleteSecretHandler(w http.ResponseWriter, r *http.Request) <span class="cov10" title="7">{ + vars := mux.Vars(r) + domName := vars["domName"] + secName := vars["secretName"] + + err := h.secretBackend.DeleteSecret(domName, secName) + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + }</span> +} + +// struct that tracks various status items for SMS and backend +type backendStatus struct { + Seal bool `json:"sealstatus"` +} + +// statusHandler returns information related to SMS and SMS backend services +func (h handler) statusHandler(w http.ResponseWriter, r *http.Request) <span class="cov6" title="3">{ + s, err := h.secretBackend.GetStatus() + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + }</span> + + <span class="cov6" title="3">status := backendStatus{Seal: s} + jdata, err := json.Marshal(status) + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + }</span> + + <span class="cov6" title="3">w.Header().Set("Content-Type", "application/json") + w.Write(jdata)</span> +} + +// loginHandler handles login via password and username +func (h handler) loginHandler(w http.ResponseWriter, r *http.Request) {<span class="cov0" title="0"> + +}</span> + +// unsealHandler is a pass through that sends requests from quorum client +// to the backend. +func (h handler) unsealHandler(w http.ResponseWriter, r *http.Request) <span class="cov0" title="0">{ + // Get shards to be used for unseal + type unsealStruct struct { + UnsealShard string `json:"unsealshard"` + } + + var inp unsealStruct + decoder := json.NewDecoder(r.Body) + decoder.DisallowUnknownFields() + err := decoder.Decode(&inp) + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + http.Error(w, "Bad input JSON", http.StatusBadRequest) + return + }</span> + + <span class="cov0" title="0">err = h.secretBackend.Unseal(inp.UnsealShard) + if err != nil </span><span class="cov0" title="0">{ + smslogger.WriteError(err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + }</span> +} + +// CreateRouter returns an http.Handler for the registered URLs +// Takes an interface implementation as input +func CreateRouter(b smsbackend.SecretBackend) http.Handler <span class="cov4" title="2">{ + h := handler{secretBackend: b} + + // Create a new mux to handle URL endpoints + router := mux.NewRouter() + + router.HandleFunc("/v1/sms/login", h.loginHandler).Methods("POST") + + // Initialization APIs which will be used by quorum client + // to unseal and to provide root token to sms service + router.HandleFunc("/v1/sms/status", h.statusHandler).Methods("GET") + router.HandleFunc("/v1/sms/unseal", h.unsealHandler).Methods("POST") + + router.HandleFunc("/v1/sms/domain", h.createSecretDomainHandler).Methods("POST") + router.HandleFunc("/v1/sms/domain/{domName}", h.deleteSecretDomainHandler).Methods("DELETE") + + router.HandleFunc("/v1/sms/domain/{domName}/secret", h.createSecretHandler).Methods("POST") + router.HandleFunc("/v1/sms/domain/{domName}/secret", h.listSecretHandler).Methods("GET") + router.HandleFunc("/v1/sms/domain/{domName}/secret/{secretName}", h.getSecretHandler).Methods("GET") + router.HandleFunc("/v1/sms/domain/{domName}/secret/{secretName}", h.deleteSecretHandler).Methods("DELETE") + + return router +}</span> +</pre> + + <pre class="file" id="file5" style="display: none">/* + * Copyright 2018 Intel Corporation, Inc + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package log + +import ( + "log" + "os" +) + +var errLogger *log.Logger +var warnLogger *log.Logger +var infoLogger *log.Logger + +// Init will be called by sms.go before any other packages use it +func Init(filePath string) <span class="cov8" title="1">{ + f, err := os.Create(filePath) + if err != nil </span><span class="cov0" title="0">{ + log.Println("Unable to create a log file") + log.Println(err) + errLogger = log.New(os.Stderr, "ERROR: ", log.Lshortfile|log.LstdFlags) + warnLogger = log.New(os.Stdout, "WARNING: ", log.Lshortfile|log.LstdFlags) + infoLogger = log.New(os.Stdout, "INFO: ", log.Lshortfile|log.LstdFlags) + }</span><span class="cov8" title="1"> else { + errLogger = log.New(f, "ERROR: ", log.Lshortfile|log.LstdFlags) + warnLogger = log.New(f, "WARNING: ", log.Lshortfile|log.LstdFlags) + infoLogger = log.New(f, "INFO: ", log.Lshortfile|log.LstdFlags) + }</span> +} + +// WriteError writes output to the writer we have +// defined durint its creation with ERROR prefix +func WriteError(msg string) <span class="cov0" title="0">{ + if errLogger != nil </span><span class="cov0" title="0">{ + errLogger.Println(msg) + }</span> +} + +// WriteWarn writes output to the writer we have +// defined durint its creation with WARNING prefix +func WriteWarn(msg string) <span class="cov0" title="0">{ + if warnLogger != nil </span><span class="cov0" title="0">{ + warnLogger.Println(msg) + }</span> +} + +// WriteInfo writes output to the writer we have +// defined durint its creation with INFO prefix +func WriteInfo(msg string) <span class="cov0" title="0">{ + if infoLogger != nil </span><span class="cov0" title="0">{ + infoLogger.Println(msg) + }</span> +} +</pre> + + <pre class="file" id="file6" style="display: none">/* + * Copyright 2018 Intel Corporation, Inc + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package main + +import ( + "context" + "log" + "net/http" + "os" + "os/signal" + + smsauth "sms/auth" + smsbackend "sms/backend" + smsconfig "sms/config" + smshandler "sms/handler" + smslogger "sms/log" +) + +func main() <span class="cov8" title="1">{ + // Initialize logger + smslogger.Init("sms.log") + + // Read Configuration File + smsConf, err := smsconfig.ReadConfigFile("smsconfig.json") + if err != nil </span><span class="cov0" title="0">{ + log.Fatal(err) + }</span> + + <span class="cov8" title="1">backendImpl, err := smsbackend.InitSecretBackend() + if err != nil </span><span class="cov0" title="0">{ + log.Fatal(err) + }</span> + + <span class="cov8" title="1">httpRouter := smshandler.CreateRouter(backendImpl) + + // TODO: Use CA certificate from AAF + tlsConfig, err := smsauth.GetTLSConfig(smsConf.CAFile) + if err != nil </span><span class="cov0" title="0">{ + log.Fatal(err) + }</span> + + <span class="cov8" title="1">httpServer := &http.Server{ + Handler: httpRouter, + Addr: ":10443", + TLSConfig: tlsConfig, + } + + // Listener for SIGINT so that it returns cleanly + connectionsClose := make(chan struct{}) + go func() </span><span class="cov8" title="1">{ + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + <-c + httpServer.Shutdown(context.Background()) + close(connectionsClose) + }</span>() + + <span class="cov8" title="1">err = httpServer.ListenAndServeTLS(smsConf.ServerCert, smsConf.ServerKey) + if err != nil && err != http.ErrServerClosed </span><span class="cov0" title="0">{ + log.Fatal(err) + }</span> + + <span class="cov8" title="1"><-connectionsClose</span> +} +</pre> + + </div> + </body> + <script> + (function() { + var files = document.getElementById('files'); + var visible; + files.addEventListener('change', onChange, false); + function select(part) { + if (visible) + visible.style.display = 'none'; + visible = document.getElementById(part); + if (!visible) + return; + files.value = part; + visible.style.display = 'block'; + location.hash = part; + } + function onChange() { + select(files.value); + window.scrollTo(0, 0); + } + if (location.hash != "") { + select(location.hash.substr(1)); + } + if (!visible) { + select("file0"); + } + })(); + </script> +</html> diff --git a/sms-service/src/sms/Gopkg.lock b/sms-service/src/sms/Gopkg.lock index 12bdfab..b856a81 100644 --- a/sms-service/src/sms/Gopkg.lock +++ b/sms-service/src/sms/Gopkg.lock @@ -53,7 +53,7 @@ branch = "master" name = "github.com/hashicorp/go-uuid" packages = ["."] - revision = "64130c7a86d732268a38cb04cfbaf0cc987fda98" + revision = "27454136f0364f2d44b1276c552d69105cf8c498" [[projects]] branch = "master" @@ -69,7 +69,7 @@ "json/scanner", "json/token" ] - revision = "23c074d0eceb2b8a5bfdbb271ab780cde70f05a8" + revision = "f40e974e75af4e271d97ce0fc917af5898ae7bda" [[projects]] name = "github.com/hashicorp/vault" @@ -80,8 +80,8 @@ "helper/parseutil", "helper/strutil" ] - revision = "36edb4d42380d89a897e7f633046423240b710d9" - version = "v0.9.5" + revision = "7e1fbde40afee241f81ef08700e7987d86fc7242" + version = "v0.9.6" [[projects]] branch = "master" @@ -93,7 +93,7 @@ branch = "master" name = "github.com/mitchellh/mapstructure" packages = ["."] - revision = "b4575eea38cca1123ec2dc90c26529b5c5acfcff" + revision = "00c29f56e2386353d58c599509e8dc3801b0d716" [[projects]] name = "github.com/ryanuber/go-glob" @@ -105,7 +105,7 @@ branch = "master" name = "github.com/sethgrid/pester" packages = ["."] - revision = "760f8913c0483b776294e1bee43f1d687527127b" + revision = "ed9870dad3170c0b25ab9b11830cc57c3a7798fb" [[projects]] branch = "master" @@ -119,7 +119,7 @@ "openpgp/packet", "openpgp/s2k" ] - revision = "85f98707c97e11569271e4d9b3d397e079c4f4d0" + revision = "88942b9c40a4c9d203b82b3731787b672d6e809b" [[projects]] branch = "master" @@ -130,10 +130,9 @@ "idna", "lex/httplex" ] - revision = "0ed95abb35c445290478a5348a7b38bb154135fd" + revision = "6078986fec03a1dcc236c34816c71b0e05018fda" [[projects]] - branch = "master" name = "golang.org/x/text" packages = [ "collate", @@ -151,11 +150,12 @@ "unicode/norm", "unicode/rangetable" ] - revision = "e19ae1496984b1c655b8044a65c0300a3c878dd3" + revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" + version = "v0.3.0" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "c3b1a2cc60523cdaccc247048d5e99d9b2f5c5f5e7f66c3e1deb9e709ec6f8bc" + inputs-digest = "5bf63627c88decba9287076c33fbda6ac39f0256039375b4002dc35db39c214f" solver-name = "gps-cdcl" solver-version = 1 diff --git a/sms-service/src/sms/Gopkg.toml b/sms-service/src/sms/Gopkg.toml index f811086..cab4149 100644 --- a/sms-service/src/sms/Gopkg.toml +++ b/sms-service/src/sms/Gopkg.toml @@ -26,7 +26,7 @@ [[constraint]] name = "github.com/hashicorp/vault" - version = "0.9.4" + version = "0.9.5" [[constraint]] branch = "master" diff --git a/sms-service/src/sms/coverage.md b/sms-service/src/sms/coverage.md new file mode 100644 index 0000000..6168342 --- /dev/null +++ b/sms-service/src/sms/coverage.md @@ -0,0 +1,41 @@ +## Code Coverage Reports for Golang Applications ## + +This document covers how to generate HTML Code Coverage Reports for +Golang Applications. + +#### Generate a test executable which calls your main() + +```sh +$ go test -c -covermode=count -coverpkg ./... +``` + +#### Run the generated application to produce a new coverage report + +```sh +$ ./sms.test -test.run "^TestMain$" -test.coverprofile=coverage.cov +``` + +#### Run your unit tests to produce their coverage report + +```sh +$ go test -test.covermode=count -test.coverprofile=unit.out ./... +``` + +#### Merge the two coverage Reports + +```sh +$ go get github.com/wadey/gocovmerge +$ gocovmerge unit.out coverage.cov > all.out +``` + +#### Generate HTML Report + +```sh +$ go tool cover -html all.out -o coverage.html +``` + +#### Generate Function Report + +```sh +$ go tool cover -func all.out +```
\ No newline at end of file diff --git a/sms-service/src/sms/sms.go b/sms-service/src/sms/sms.go index eb0bebc..de9d0a7 100644 --- a/sms-service/src/sms/sms.go +++ b/sms-service/src/sms/sms.go @@ -17,8 +17,11 @@ package main import ( + "context" "log" "net/http" + "os" + "os/signal" smsauth "sms/auth" smsbackend "sms/backend" @@ -56,6 +59,20 @@ func main() { TLSConfig: tlsConfig, } + // Listener for SIGINT so that it returns cleanly + connectionsClose := make(chan struct{}) + go func() { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + <-c + httpServer.Shutdown(context.Background()) + close(connectionsClose) + }() + err = httpServer.ListenAndServeTLS(smsConf.ServerCert, smsConf.ServerKey) - log.Fatal(err) + if err != nil && err != http.ErrServerClosed { + log.Fatal(err) + } + + <-connectionsClose } diff --git a/sms-service/src/sms/sms_test.go b/sms-service/src/sms/sms_test.go new file mode 100644 index 0000000..35dce24 --- /dev/null +++ b/sms-service/src/sms/sms_test.go @@ -0,0 +1,31 @@ +/* + * Copyright 2018 Intel Corporation, Inc + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package main + +import ( + "fmt" + "os" + "testing" +) + +func TestMain(t *testing.T) { + fmt.Println(os.Args) + t.Log(os.Args) + if os.Args[0] == "./sms.test" { + main() + } +} |