// Package passkey provides a client for retrieving secret data from password safe package passkey import ( "encoding/json" "errors" "fmt" "io/ioutil" "log" "net/http" "os" "strings" "github.com/julienschmidt/httprouter" "github.rackspace.com/gorax/clients/passwordsafe" "github.rackspace.com/gorax/webutil" ) const ( // dcmProjID is the passwordsafe Project for DC Migrations credentials dcmProjID = 20834 nsiProdEnvProjID = 5704 netsecDevopsProjID = 2962 svcAccounts = 19 apiKeys = 20 dcmCreds = 5 envUserKey = "PASSKEY_USER" envPwdKey = "PASSKEY_PW" defaultFile = "/srv/config/credentials.json" dockerSecret = "/run/secrets/credentials.json" ) type ( fileCred struct { Desc string `json:"description"` User string `json:"username"` Pass string `json:"password"` } fileContents struct { Creds []fileCred `json:"credentials"` } ) type auth struct { Password string `json:"password"` Username string `json:"username"` XAuthToken string `json:"X-Auth-Token"` } // Client : The main password safe client type Client struct { Authed bool AuthMethod string Keys APIKeys pwsafe *passwordsafe.Client Service ServiceAccounts DCM DCMCreds } type ErrKeyNotFound struct { Key int } func (e ErrKeyNotFound) Error() string { return fmt.Sprintf("key %d not found", e.Key) } type ServiceAccounts struct { CloudDB passwordsafe.Credential RSCloud passwordsafe.Credential Netsec01 passwordsafe.Credential Netsec02 passwordsafe.Credential SSO passwordsafe.Credential TacacsEnable passwordsafe.Credential TacacsLogin passwordsafe.Credential TestTacacsEnable passwordsafe.Credential TestTacacsLogin passwordsafe.Credential Unipede passwordsafe.Credential GDCIDataMart passwordsafe.Credential Imperva passwordsafe.Credential F5Default passwordsafe.Credential DPTacacsLogin passwordsafe.Credential MyDPSvc passwordsafe.Credential ADNetSecAAA passwordsafe.Credential ISEAAAAPI passwordsafe.Credential GlobalRSISE passwordsafe.Credential } type APIKeys struct { WildcardCABundle passwordsafe.Credential WildcardCert passwordsafe.Credential WildcardKey passwordsafe.Credential ACSFreeIPA passwordsafe.Credential AltDBAPI passwordsafe.Credential APIKey passwordsafe.Credential CoreAPI passwordsafe.Credential DBAPI passwordsafe.Credential DCX passwordsafe.Credential EntNewRelic passwordsafe.Credential Insights passwordsafe.Credential JWT passwordsafe.Credential Mailgun passwordsafe.Credential NagiosRedis passwordsafe.Credential NewRelic passwordsafe.Credential Redis passwordsafe.Credential ServiceTokens passwordsafe.Credential WebSvcsKey passwordsafe.Credential SAML struct { MigrationCert passwordsafe.Credential MigrationKey passwordsafe.Credential } Mongo struct { // Mocks nsi_mongo_user in passwordsafe NSIMongoUser passwordsafe.Credential } } // DCMCreds type is a definition of credentials used by DC Migrations services type DCMCreds struct { BigQuery passwordsafe.Credential SSO passwordsafe.Credential DPOne passwordsafe.Credential Network passwordsafe.Credential MyAdmin passwordsafe.Credential } // APIInit : Will try to initialize the client using environment variables, docker secrets, and plain credentials.json file in that order func APIInit(staging bool) (*Client, error) { // try environment variables first if user, pass, ok := envCreds(); ok { return New(user, pass, staging) } // try docker secrets next pk, err := NewFromFile("netsec01", dockerSecret, staging) if err == nil { return pk, nil } // finally try older credentials.json file location pk, err = NewFromFile("netsec01", defaultFile, staging) if err == nil { return pk, nil } return nil, errors.New("unable to initialize passkey via environment variables, docker secret, or credentials.json file") } // New : Initializes a new client using the given username and password func New(user string, passwd string, staging bool) (*Client, error) { pk := &Client{ Authed: false, AuthMethod: "Username/Password\"", } if user == "" || passwd == "" { return nil, fmt.Errorf("username and password required") } p, err := passwordsafe.NewClient(user, passwd) if err != nil { return nil, err } pk.pwsafe = p if err := pk.gather(staging); err != nil { return pk, err } pk.Authed = true return pk, nil } // NewFromFile : Initializes a new client by using the credentials json file at the location passed. If no location is given it will default to: "/srv/config/credentials.json" func NewFromFile(user string, file string, staging bool) (*Client, error) { pk := &Client{ Authed: false, AuthMethod: "Username/Password_FromFile", } d, err := pk.getFileContents(file) if err != nil { return nil, err } for _, cred := range d.Creds { if strings.Contains(strings.ToLower(cred.User), strings.ToLower(user)) { pk.pwsafe, err = passwordsafe.NewClient(cred.User, cred.Pass) if err != nil { return pk, err } if err := pk.gather(staging); err != nil { return pk, err } return pk, nil } } return pk, errors.New("user does not exist in file") } // NewFromEnvironment : Initializes a new client using the 'user' and 'pass' environment variables func NewFromEnvironment(staging bool) (*Client, error) { user, pass, ok := envCreds() if !ok { return nil, errors.New("required environment variables 'user' and or 'pass' not found") } return New(user, pass, staging) } func (pk *Client) getFileContents(file string) (fileContents, error) { fullPath := "/srv/config/credentials.json" if file != "" { fullPath = file } var c fileContents raw, err := ioutil.ReadFile(fullPath) if err != nil { return c, err } if err := json.Unmarshal(raw, &c); err != nil { return c, err } return c, nil } func (pk *Client) gather(staging bool) error { if err := pk.getServiceAccounts(staging); err != nil { return err } if err := pk.getAPIKeys(staging); err != nil { return err } if err := pk.getDCMCreds(staging); err != nil { return err } pk.Authed = true return nil } func (pk *Client) AuthPOST(rw http.ResponseWriter, r *http.Request, _ httprouter.Params) { payload := new(auth) decoder := json.NewDecoder(r.Body) if err := decoder.Decode(payload); err != nil { webutil.ErrHandler(rw, r, webutil.Response{"authenticated": false, "message": "json decode failed"}, http.StatusBadRequest) return } if payload.XAuthToken != "" { p, err := passwordsafe.NewTokenClient(payload.XAuthToken) if err != nil { webutil.ErrHandler(rw, r, webutil.Response{"authenticated": false, "message": "Unable to authenticate"}, http.StatusUnauthorized) return } pk.pwsafe = p if err := pk.getServiceAccounts(false); err != nil { webutil.ErrHandler(rw, r, webutil.Response{"authenticated": false, "message": err.Error()}, http.StatusBadRequest) return } if err := pk.getAPIKeys(false); err != nil { webutil.ErrHandler(rw, r, webutil.Response{"authenticated": false, "message": err.Error()}, http.StatusBadRequest) return } pk.Authed = true pk.AuthMethod = "API /auth POST" webutil.ResHandler(rw, r, webutil.Response{"authenticated": true, "message": "application authenticated"}) return } if payload.Username == "" { webutil.ErrHandler(rw, r, webutil.Response{"authenticated": false, "message": "Username required"}, http.StatusUnauthorized) return } if payload.Password == "" { webutil.ErrHandler(rw, r, webutil.Response{"authenticated": false, "message": "Password required"}, http.StatusUnauthorized) return } p, err := passwordsafe.NewClient(payload.Username, payload.Password) if err != nil { webutil.ErrHandler(rw, r, webutil.Response{"authenticated": false, "message": "Unable to authenticate"}, http.StatusUnauthorized) return } pk.pwsafe = p if err := pk.getServiceAccounts(false); err != nil { webutil.ErrHandler(rw, r, webutil.Response{"authenticated": false, "message": err.Error()}, http.StatusBadRequest) return } if err := pk.getAPIKeys(false); err != nil { webutil.ErrHandler(rw, r, webutil.Response{"authenticated": false, "message": err.Error()}, http.StatusBadRequest) return } pk.Authed = true pk.AuthMethod = "API /auth POST" webutil.ResHandler(rw, r, webutil.Response{"authenticated": true, "message": "application authenticated"}) } func (pk *Client) AuthGET(rw http.ResponseWriter, r *http.Request, _ httprouter.Params) { if !pk.Authed { webutil.ResHandler(rw, r, webutil.Response{"authenticated": false, "message": "application not authenticated", "data": howTo()}) return } webutil.ResHandler(rw, r, webutil.Response{"authenticated": true, "message": fmt.Sprintf("application authenticated via %v", pk.AuthMethod)}) } func (pk *Client) getServiceAccounts(staging bool) error { cr, err := pk.getCredentialsByProjectID(netsecDevopsProjID) if err != nil { return err } var cnt int for _, cred := range cr { switch cred.Credential.Id { case 19134: cnt++ pk.Service.TacacsLogin = cred case 59990: cnt++ pk.Service.RSCloud = cred case 19136: cnt++ pk.Service.TacacsEnable = cred case 32988: if !staging { cnt++ pk.Service.Netsec01 = cred } case 32990: if !staging { cnt++ pk.Service.Netsec02 = cred } case 129361: if staging { cnt++ pk.Service.Netsec01 = cred } case 129363: if staging { cnt++ pk.Service.Netsec02 = cred } case 57048: cnt++ pk.Service.SSO = cred case 114180: cnt++ pk.Service.TestTacacsLogin = cred case 114182: cnt++ pk.Service.TestTacacsEnable = cred case 130263: cnt++ pk.Service.Unipede = cred case 159954: cnt++ pk.Service.GDCIDataMart = cred case 111077: cnt++ pk.Service.CloudDB = cred case 168228: cnt++ pk.Service.Imperva = cred case 168398: cnt++ pk.Service.F5Default = cred case 185998: cnt++ pk.Service.DPTacacsLogin = cred case 188329: cnt++ pk.Service.MyDPSvc = cred case 199731: cnt++ pk.Service.ISEAAAAPI = cred case 198564: cnt++ pk.Service.ADNetSecAAA = cred case 198559: cnt++ pk.Service.GlobalRSISE = cred case 200383: if !staging { cnt++ pk.Keys.Mongo.NSIMongoUser = cred } case 208854: if staging { cnt++ pk.Keys.Mongo.NSIMongoUser = cred } } } if cnt != svcAccounts { return errors.New("unable to gather all service accounts") } return nil } func (pk *Client) getAPIKeys(staging bool) error { cr, err := pk.getCredentialsByProjectID(nsiProdEnvProjID) if err != nil { return err } var cnt int if staging { for _, cred := range cr { switch cred.Credential.Id { case 130219: cnt++ pk.Keys.APIKey = cred case 189939: cnt++ pk.Keys.WildcardCABundle = cred case 189942: cnt++ pk.Keys.WildcardKey = cred case 189937: cnt++ pk.Keys.WildcardCert = cred case 94482: cnt++ pk.Keys.ACSFreeIPA = cred case 94433: cnt++ pk.Keys.CoreAPI = cred case 94281: cnt++ pk.Keys.DBAPI = cred case 68630: cnt++ pk.Keys.AltDBAPI = cred case 94473: cnt++ pk.Keys.ServiceTokens = cred case 67610: cnt++ pk.Keys.Redis = cred case 145995: cnt++ pk.Keys.NewRelic = cred case 146361: cnt++ pk.Keys.Mailgun = cred case 157461: cnt++ pk.Keys.WebSvcsKey = cred case 162190: cnt++ pk.Keys.Insights = cred case 188452: cnt++ pk.Keys.EntNewRelic = cred case 95302: cnt++ pk.Keys.DCX = cred case 211932: cnt++ pk.Keys.JWT = cred case 221185: cnt++ pk.Keys.NagiosRedis = cred case 223494: cnt++ pk.Keys.SAML.MigrationCert = cred case 223495: cnt++ pk.Keys.SAML.MigrationKey = cred } } if cnt != apiKeys { return errors.New("unable to gather all API Keys") } return nil } for _, cred := range cr { switch cred.Credential.Id { case 61600: cnt++ pk.Keys.APIKey = cred case 189939: cnt++ pk.Keys.WildcardCABundle = cred case 189942: cnt++ pk.Keys.WildcardKey = cred case 189937: cnt++ pk.Keys.WildcardCert = cred case 79801: cnt++ pk.Keys.ACSFreeIPA = cred case 68789: cnt++ pk.Keys.CoreAPI = cred case 68630: cnt++ pk.Keys.DBAPI = cred case 94281: cnt++ pk.Keys.AltDBAPI = cred case 64212: cnt++ pk.Keys.ServiceTokens = cred case 61179: cnt++ pk.Keys.Redis = cred case 145995: cnt++ pk.Keys.NewRelic = cred case 146361: cnt++ pk.Keys.Mailgun = cred case 157459: cnt++ pk.Keys.WebSvcsKey = cred case 162190: cnt++ pk.Keys.Insights = cred case 188452: cnt++ pk.Keys.EntNewRelic = cred case 80808: cnt++ pk.Keys.DCX = cred case 211932: cnt++ pk.Keys.JWT = cred case 221185: cnt++ pk.Keys.NagiosRedis = cred case 223494: cnt++ pk.Keys.SAML.MigrationCert = cred case 223495: cnt++ pk.Keys.SAML.MigrationKey = cred } } if cnt != apiKeys { return errors.New("unable to gather all API Keys") } return nil } func (pk *Client) getDCMCreds(staging bool) error { cr, err := pk.getCredentialsByProjectID(dcmProjID) if err != nil { return err } var ( dpOneID = 212687 dpNetworkID = 192170 dpMyAdminID = 181546 dcmSSOID = 248276 dcmBigQuery = 253501 ) if staging { dpOneID = 211231 } var cnt int for _, cred := range cr { switch cred.Credential.Id { case dpOneID: cnt++ pk.DCM.DPOne = cred case dpNetworkID: cnt++ pk.DCM.Network = cred case dpMyAdminID: cnt++ pk.DCM.MyAdmin = cred case dcmSSOID: cnt++ pk.DCM.SSO = cred case dcmBigQuery: cnt++ pk.DCM.BigQuery = cred } } if cnt != dcmCreds { return errors.New("unable to gather all DC Migrations credentials") } return nil } func (pk *Client) getCredentialsByProjectID(id int) ([]passwordsafe.Credential, error) { pl, err := pk.pwsafe.ProjectList() if err != nil { return nil, err } for _, proj := range pl { if proj.Project.Id == id { cr, err := pk.pwsafe.ProjectCredentials(proj) if err != nil { return nil, err } return cr, nil } } return nil, errors.New("unable to find project") } func envCreds() (string, string, bool) { user, ok := os.LookupEnv(envUserKey) if !ok { return "", "", false } pass, ok := os.LookupEnv(envPwdKey) if !ok { return "", "", false } return user, pass, true } func howTo() *map[string]interface{} { m := make(map[string]interface{}) s := []byte(` { "baseUrl": "", "uri": [ { "location": "/auth", "desc": "authenticate a container for in-code credentials (ie. passwordsafe)", "methods": { "POST": { "request": { "username": "(string|nil)", "password": "(string|nil)" }, "response": { "authenticated": "(bool|false)", "message": "(string|application authenticated)" } }, "GET": { "response": [ { "authenticated": "(bool|false)", "message": "(string|application not authenticated)", "data": "(json|this help message)" }, { "authenticated": "(bool|true)", "message": "(string|application authenticated)" } ] } } } ] }`) if err := json.Unmarshal(s, &m); err != nil { log.Fatal(err) } return &m }