Commit ccc09759 authored by Lukas Burgey's avatar Lukas Burgey
Browse files

Implement sophisticated key negotiation

Closes #7
parent 342b7433
......@@ -25,7 +25,7 @@ import (
var (
app = kingpin.New(
"FEUDAL SSH",
"SSH intergration for FEUDAL",
"SSH integration for FEUDAL",
).Author(
"Lukas Burgey",
).Version(
......@@ -41,7 +41,7 @@ var (
issuerURI = app.Flag("issuer", "Issuer URI of your access token").Short('i').Default("https://unity.helmholtz-data-federation.de/oauth2").String()
feudalURI = app.Flag("uri", "Feudal URI").Short('u').Default("https://hdf-portal-dev.data.kit.edu").String()
pubKey = app.Flag("pubkey", "SSH public key file path").Short('k').Required().String()
pubKey = app.Flag("pubkey", "SSH public key file path").Short('k').String()
)
func restCall(method string, path string, body io.Reader) (responseBytes []byte, err error) {
......@@ -96,65 +96,152 @@ func fetchDepState(id int) (state api.DepState) {
return
}
func readPubKey() (name, key string) {
// determine which file to use
path := *pubKey
switch filepath.Ext(path) {
case ".pub":
// all nice and good
case "":
// add extension (probably a private key file was provided)
path += ".pub"
default:
// unusable extension
log.Fatalf("Unusable file: %s", path)
// select a local key for reading
func selectLocalKey() (selected string) {
var sshDir = filepath.Join(os.ExpandEnv("$HOME"), ".ssh")
// check if the user gave a valid argument
if *pubKey != "" {
if filepath.Ext(*pubKey) == ".pub" {
if _, err := os.Stat(*pubKey); os.IsExist(err) {
selected = *pubKey
return
}
} else {
if _, err := os.Stat(*pubKey + ".pub"); os.IsExist(err) {
selected = *pubKey + ".pub"
return
}
}
}
if _, err := os.Stat(path); err != nil {
if os.IsNotExist(err) {
log.Fatalf("File does not exist: %s", path)
// pick a local key
localKeys, err := ioutil.ReadDir(sshDir)
if err != nil {
log.Fatalf("Reading SSH dir: %s", err)
}
if len(localKeys) > 0 {
var keyMap = map[string]struct{}{}
for _, localKey := range localKeys {
keyMap[localKey.Name()] = struct{}{}
}
// check preferred keys
for _, key := range []string{
"id_ed25519",
"id_rsa",
"id_ecdsa",
"id_dsa",
} {
// check if key the pair exists
if _, ok := keyMap[key]; ok {
if _, ok := keyMap[key+".pub"]; ok {
return filepath.Join(sshDir, key+".pub")
}
}
}
// no preferred keys were found, so we take the first available key
for keyName := range keyMap {
if filepath.Ext(keyName) == ".pub" {
// does the corresponding private key exist?
privateKeyName := strings.TrimSuffix(keyName, ".pub")
if _, ok := keyMap[privateKeyName]; ok {
return filepath.Join(sshDir, keyName)
}
}
}
log.Fatal(err)
}
keyBytes, err := ioutil.ReadFile(path)
// the ssh dir is empty!?
log.Fatalf("Please specify an SSH public key")
return
}
func readPubKey() (name, key string) {
selected := selectLocalKey()
name = filepath.Base(selected)
keyBytes, err := ioutil.ReadFile(selected)
if err != nil {
log.Fatal(err)
}
name = filepath.Base(path)
key = string(keyBytes)
return
}
// keyExists check if the given key / name combination was already uploaded to feudal
func keyExists(name, key string) bool {
func fetchUpstreamKeys() (keys []api.SSHKey, err error) {
// fetch key list
keyListResponse, err := restCall("GET", "ssh-keys", nil)
if err != nil {
return false
log.Printf("Fetching upstream keys: %s", err)
return
}
var sshKeys = []api.SSHKey{}
err = json.Unmarshal(keyListResponse, &sshKeys)
err = json.Unmarshal(keyListResponse, &keys)
if err != nil {
return false
return
}
for _, k := range sshKeys {
// the backend strips comments so we adjust our check
if k.Name == name && strings.HasPrefix(key, k.Key) {
return true
return
}
// make sure at least one public key for which we have the private key is uploaded at the backend
func negotiatePublicKey() (selectedKey api.SSHKey) {
// keys at feudal
upstreamKeys, err := fetchUpstreamKeys()
if err != nil {
}
// ssh dir
homeDir, _ := os.UserHomeDir()
sshPath := filepath.Join(homeDir, ".ssh")
log.Printf("SSH path: %s", sshPath)
localKeys, err := ioutil.ReadDir(sshPath)
if err != nil {
log.Fatalf("Reading SSH dir: %s", err)
}
// load keys from the users .ssh dir
var localKeyMap = make(map[string]string)
for _, localKey := range localKeys {
if filepath.Ext(localKey.Name()) == ".pub" {
name := filepath.Base(localKey.Name())
keyBytes, err := ioutil.ReadFile(filepath.Join(sshPath, localKey.Name()))
if err == nil {
localKeyMap[name] = string(keyBytes)
}
}
}
return false
}
func uploadPubKey() (state api.DepState) {
// check if we have any of them
for _, upstreamKey := range upstreamKeys {
if key, ok := localKeyMap[upstreamKey.Name]; ok && strings.HasPrefix(key, upstreamKey.Key) {
selectedKey = upstreamKey
log.Printf("Upstream has key:\t%s (found locally)", upstreamKey.Name)
return
}
}
name, key := readPubKey()
if keyExists(name, key) {
log.Printf("Key already uploaded: %s", name)
return
// check we have any of them with different filenames
for _, upstreamKey := range upstreamKeys {
for lName, lKey := range localKeyMap {
if lKey == upstreamKey.Key {
selectedKey = api.SSHKey{
Name: lName,
Key: lKey,
}
log.Printf("Upstream has key:\t%s (found locally as %s)", upstreamKey.Name, lName)
return
}
}
}
log.Printf("We have no common key with upstream")
// we need to upload a key
name, key := readPubKey()
log.Printf("Uploading key %s", name)
body := map[string]string{"name": name, "key": key}
......@@ -162,18 +249,17 @@ func uploadPubKey() (state api.DepState) {
if err != nil {
log.Fatal(err)
}
response, err := restCall("POST", "ssh-keys", bytes.NewReader(bodyBytes))
_, err = restCall("POST", "ssh-keys", bytes.NewReader(bodyBytes))
if err != nil {
log.Fatalf("Uploading key: %s", err)
}
if responseIdented, err := json.MarshalIndent(response, "", " "); err == nil {
log.Printf("Response: %s", responseIdented)
} else {
log.Printf("Response: %s", response)
return api.SSHKey{
Name: name,
Key: key,
}
return
}
// determined the id of the requested service
func findServiceID() (serviceID int) {
serviceBytes, err := restCall("GET", "services", nil)
if err != nil {
......@@ -203,7 +289,7 @@ func findServiceID() (serviceID int) {
for _, s := range services {
if s.Name == *serviceName {
serviceID = s.ID
log.Printf("Selected service '%s' (id: %d)", *serviceName, serviceID)
log.Printf("Selected service:\t%s", *serviceName)
return
}
}
......@@ -286,9 +372,9 @@ func inspectAccessToken(at string) (issuerURI string) {
}
var payloadBytesIndented = bytes.NewBuffer([]byte{})
if json.Indent(payloadBytesIndented, payloadBytes, "", " ") == nil {
log.Printf("JWT access token payload: %s", payloadBytesIndented.Bytes())
log.Printf("JWT access token payload:\n%s", payloadBytesIndented.Bytes())
} else {
log.Printf("JWT access token payload: %s", payloadBytes)
log.Printf("JWT access token payload:\n%s", payloadBytes)
}
payload := map[string]interface{}{}
if err := json.Unmarshal(payloadBytes, &payload); err != nil {
......@@ -318,9 +404,11 @@ func main() {
}
log.Printf("Using issuer: %s", *issuerURI)
var serviceID = findServiceID()
negotiatedKey := negotiatePublicKey()
*pubKey = negotiatedKey.Name
log.Printf("Selected ssh key:\t%s", *pubKey)
uploadPubKey()
var serviceID = findServiceID()
// start the deployment
var deployment api.Deployment
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment