// Package passkey provides methods for retrieving credentials from password safe package passkey import ( "encoding/json" "errors" "fmt" "io/ioutil" "log" "net/http" "os" "strings" "github.com/julienschmidt/httprouter" "github.com/sirupsen/logrus" "github.rackspace.com/gorax/clients/passwordsafe" "github.rackspace.com/gorax/tools/lagger" "github.rackspace.com/gorax/tools/webutil" ) const ( svcAccounts = 19 apiKeys = 19 dpCreds = 3 // DatapipeServiceAccounts is the passwordsafe // Project for Datapipe Credentials DatapipeServiceAccounts = 20834 ) const ( envUserKey = "PASSKEY_USER" envPwdKey = "PASSKEY_PW" ) type Auth struct { Password string `json:"password"` Username string `json:"username"` XAuthToken string `json:"X-Auth-Token"` } type Client struct { Authed bool AuthMethod string Keys APIKeys log lagger.NSILogger pwsafe *passwordsafe.Client Service ServiceAccounts DP DatapipeCreds } 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 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 } } // DatapipeCreds type is a definition of credentials used to // access legacy Datapipe services type DatapipeCreds struct { DPOne passwordsafe.Credential Network passwordsafe.Credential MyAdmin passwordsafe.Credential } type FileContents struct { Creds []FileCred `json:"credentials"` } type FileCred struct { Desc string `json:"description"` User string `json:"username"` Pass string `json:"password"` } // New initializes a new Passkey object using the given username and password func New(user string, passwd string, staging bool, logger ...lagger.NSILogger) (*Client, error) { if len(logger) == 0 { // They didn't pass an already initialized logger, let's make our own lag, err := lagger.New("passkey-library", "passkey", logrus.InfoLevel, false) if err != nil { return nil, fmt.Errorf("no NSILogger passed and unable to initialize new one: %s", err.Error()) } logger = append(logger, *lag) } pk := &Client{ Authed: false, log: logger[0], AuthMethod: "Username/Password_FromVars", } if user == "" || passwd == "" { return nil, fmt.Errorf("username and password required") } p, err := passwordsafe.NewClient(user, passwd) if err != nil { pk.log.Warn(pk.log.AppMessage(fmt.Sprintf("error initiating passwordsafe client with user %v (%v)", user, err))) return nil, err } pk.pwsafe = p if err := pk.gather(staging); err != nil { return pk, err } pk.Authed = true pk.AuthMethod = "Username/Password" return pk, nil } // NewFromFile initializes the passwordsafe 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, logger ...lagger.NSILogger) (*Client, error) { if len(logger) == 0 { // They didn't pass an already initialized logger, let's make our own lag, err := lagger.New("passkey-library", "passkey", logrus.InfoLevel, false) if err != nil { return nil, fmt.Errorf("no NSILogger passed and unable to initialize new one: %s", err.Error()) } logger = append(logger, *lag) } pk := &Client{ Authed: false, log: logger[0], 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 { pk.log.Warn(pk.log.AppMessage(fmt.Sprintf("passkey/NewFromFile() error initiating client with %v (%v)", cred.User, err))) 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") } 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 { pk.log.Error(pk.log.AppMessage(fmt.Sprintf("error reading from file %v", err.Error()))) 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 { pk.log.Warn(pk.log.AppMessage(fmt.Sprintf("error gathering service accounts (%v)", err))) return err } if err := pk.GetAPIKeys(staging); err != nil { pk.log.Warn(pk.log.AppMessage(fmt.Sprintf("error gathering API Keys (%v)", err))) return err } if err := pk.GetDPCreds(staging); err != nil { pk.log.Warn(pk.log.AppMessage(fmt.Sprintf("error gathering legacy Datapipe credentials (%v)", err))) return err } pk.Authed = true return nil } func (pk *Client) AuthPOST(rw http.ResponseWriter, r *http.Request, _ httprouter.Params) { a := new(Auth) d := json.NewDecoder(r.Body) if err := d.Decode(a); err != nil { webutil.ErrHandler(rw, r, webutil.Response{"authenticated": false, "message": "json decode failed"}, 401) return } if a.XAuthToken != "" { p, err := passwordsafe.NewTokenClient(a.XAuthToken) if err != nil { pk.log.Warn(pk.log.WebLog(r, a.Username, "failed application auth attempt with X-Auth-Token")) webutil.ErrHandler(rw, r, webutil.Response{"authenticated": false, "message": "Unable to authenticate"}, 401) return } pk.pwsafe = p if err := pk.GetServiceAccounts(false); err != nil { webutil.ErrHandler(rw, r, webutil.Response{"authenticated": false, "message": err.Error()}, 400) return } if err := pk.GetAPIKeys(false); err != nil { webutil.ErrHandler(rw, r, webutil.Response{"authenticated": false, "message": err.Error()}, 400) return } pk.Authed = true pk.AuthMethod = "API /auth POST" pk.log.Info(pk.log.WebLog(r, a.Username, fmt.Sprintf("application authenticated by %v", a.Username))) webutil.ResHandler(rw, r, webutil.Response{"authenticated": true, "message": "application authenticated"}) return } if a.Username == "" { webutil.ErrHandler(rw, r, webutil.Response{"authenticated": false, "message": "Username required"}, 401) return } if a.Password == "" { webutil.ErrHandler(rw, r, webutil.Response{"authenticated": false, "message": "Password required"}, 401) return } p, err := passwordsafe.NewClient(a.Username, a.Password) if err != nil { pk.log.Warn(pk.log.WebLog(r, a.Username, fmt.Sprintf("failed application auth attempt by %v", a.Username))) webutil.ErrHandler(rw, r, webutil.Response{"authenticated": false, "message": "Unable to authenticate"}, 401) return } pk.pwsafe = p if err := pk.GetServiceAccounts(false); err != nil { webutil.ErrHandler(rw, r, webutil.Response{"authenticated": false, "message": err.Error()}, 400) return } if err := pk.GetAPIKeys(false); err != nil { webutil.ErrHandler(rw, r, webutil.Response{"authenticated": false, "message": err.Error()}, 400) return } pk.Authed = true pk.AuthMethod = "API /auth POST" pk.log.Info(pk.log.WebLog(r, a.Username, fmt.Sprintf("Application authenticated by %v", a.Username))) webutil.ResHandler(rw, r, webutil.Response{"authenticated": true, "message": "application authenticated"}) return } func (pk *Client) AuthGET(rw http.ResponseWriter, r *http.Request, _ httprouter.Params) { if pk.Authed != true { 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)}) return } func howTo() *map[string]interface{} { m := make(map[string]interface{}) s := []byte(` { "baseUrl": "", "uri": [ { "location": "/auth", "desc": "authenitcate 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 } func (pk *Client) GetServiceAccounts(staging bool) error { cr, err := pk.getCredentialsByProjectID(2962) 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(5704) if err != nil { return err } var cnt int if staging { for _, cred := range cr { switch cred.Credential.Id { 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 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) GetDPCreds(staging bool) error { cr, err := pk.getCredentialsByProjectID(DatapipeServiceAccounts) if err != nil { return err } var ( dpOneID = 212687 dpNetworkID = 192170 dpMyAdminID = 181546 ) if staging { dpOneID = 211231 } var cnt int for _, cred := range cr { switch cred.Credential.Id { case dpOneID: cnt++ pk.DP.DPOne = cred case dpNetworkID: cnt++ pk.DP.Network = cred case dpMyAdminID: cnt++ pk.DP.MyAdmin = cred } } if cnt != dpCreds { return errors.New("unable to gather all legacy Datapipe 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") } // EnvCreds : Returns the username and password found in environment variables for passkey, plus a boolean on if they were found or not. // This is useful for first trying to initialize passkey via the variables and then falling back to file method. // // Example: // var *passkey.Client // // func initPasskey() { // var err error // if user, pwd, ok := passkey.EnvCreds(); ok { // log.Debug("Using environment variables/username to initialize passkey") // pk, err = passkey.New(user, pwd, staging) // if err != nil { // log.Panic(err) // } // return // } // // log.Debug("Using file to initialize passkey") // pk, err = passkey.NewFromFile("netsec01", "", staging) // if err != nil { // log.Panic(err) // } // } 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 }