Commit 8ab551df authored by Lukas Burgey's avatar Lukas Burgey
Browse files

Merge branch 'dev'

parents 9a4a20d6 b3f5c1bc
......@@ -5,6 +5,7 @@ import "fmt"
type SSHKey struct {
Name string `json:"name"`
Key string `json:"key"`
File string
}
type Service struct {
......
#!/bin/sh
cmdLine=$( feudalSSH $* )
[[ $? -eq 0 ]] && eval $cmdLine
......@@ -9,7 +9,6 @@ import (
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"text/tabwriter"
......@@ -17,7 +16,7 @@ import (
api "git.scc.kit.edu/feudal/feudalSSH/api"
jwt "github.com/dgrijalva/jwt-go"
sshclient "github.com/helloyi/go-sshclient"
"golang.org/x/crypto/ssh"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
......@@ -25,7 +24,7 @@ import (
var (
app = kingpin.New(
"FEUDAL SSH",
"SSH intergration for FEUDAL",
"SSH integration for FEUDAL",
).Author(
"Lukas Burgey",
).Version(
......@@ -33,15 +32,21 @@ var (
)
httpClient = &http.Client{}
serviceName = app.Arg("serviceName", "Service name to ssh to").String()
serviceID = app.Arg("serviceID", "ID of service to use").Int()
// access token, possibly from the environment
// Envar OIDC has precedence over OIDC_AT
accessToken = app.Flag("at", "Access Token").Short('a').Envar("OIDC_AT").Envar("OIDC").Required().String()
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()
issuerURIArg = app.Flag("issuer", "Issuer URI of your access token. Not needed if JWT access token with iss claim.").Short('i').Default("https://unity.helmholtz-data-federation.de/oauth2").String()
feudalURI = app.Flag("uri", "Feudal URI").Short('u').Default("https://hdf-portal.data.kit.edu").String()
pubKey = app.Flag("pubkey", "SSH public key file path").Short('k').String()
verboseArg = app.Flag("verbose", "Increase verbosity").Short('v').Bool()
connectArg = app.Flag("connect", "Open SSH connection, once credentials are received.").Short('c').Bool()
issuerURI = ""
sshSession *ssh.Session
)
func restCall(method string, path string, body io.Reader) (responseBytes []byte, err error) {
......@@ -49,7 +54,7 @@ func restCall(method string, path string, body io.Reader) (responseBytes []byte,
if err != nil {
return
}
request.Header.Add("X-Issuer", *issuerURI)
request.Header.Add("X-Issuer", issuerURI)
request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", *accessToken))
if body != nil {
......@@ -96,65 +101,152 @@ func fetchDepState(id int) (state api.DepState) {
return
}
func readPubKey() (name, key string) {
// determine which file to use
// select a local key for reading
func selectLocalKey(userArg string) (selected string) {
var sshDir = filepath.Join(os.ExpandEnv("$HOME"), ".ssh")
// check if the user gave a valid argument
if userArg != "" {
if filepath.Ext(userArg) == ".pub" {
if _, err := os.Stat(userArg); os.IsExist(err) {
selected = userArg
return
}
} else {
if _, err := os.Stat(userArg + ".pub"); os.IsExist(err) {
selected = userArg + ".pub"
return
}
}
}
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)
// pick a local key
localKeys, err := ioutil.ReadDir(sshDir)
if err != nil {
log.Fatalf("Reading SSH dir: %s", err)
}
if _, err := os.Stat(path); err != nil {
if os.IsNotExist(err) {
log.Fatalf("File does not exist: %s", path)
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(userArg string) (name, key string) {
selected := selectLocalKey(userArg)
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
}
err = json.Unmarshal(keyListResponse, &keys)
if err != nil {
return
}
return
}
// make sure at least one public key for which we have the private key is uploaded at the backend
func negotiatePublicKey(userArg string) (selectedKey api.SSHKey) {
// keys at feudal
upstreamKeys, err := fetchUpstreamKeys()
if err != nil {
}
var sshKeys = []api.SSHKey{}
err = json.Unmarshal(keyListResponse, &sshKeys)
// ssh dir
homeDir, _ := os.UserHomeDir()
sshPath := filepath.Join(homeDir, ".ssh")
localKeys, err := ioutil.ReadDir(sshPath)
if err != nil {
return false
log.Fatalf("Reading SSH dir: %s", err)
}
for _, k := range sshKeys {
// the backend strips comments so we adjust our check
if k.Name == name && strings.HasPrefix(key, k.Key) {
return true
// load keys from the users .ssh dir
var localKeyMap = make(map[string]api.SSHKey)
for _, localKey := range localKeys {
if filepath.Ext(localKey.Name()) == ".pub" {
file := filepath.Join(sshPath, localKey.Name())
keyBytes, err := ioutil.ReadFile(file)
if err == nil {
localKeyMap[localKey.Name()] = api.SSHKey{
Name: filepath.Base(localKey.Name()),
Key: string(keyBytes),
File: file,
}
}
}
}
return false
}
func uploadPubKey() (state api.DepState) {
// check if we have any of them:
// A) by filename
for _, upstreamKey := range upstreamKeys {
if key, ok := localKeyMap[upstreamKey.Name]; ok && strings.HasPrefix(key.Key, upstreamKey.Key) {
selectedKey = upstreamKey
log.Printf("Upstream has key: %s (found in %s)", upstreamKey.Name, sshPath)
return
}
}
name, key := readPubKey()
if keyExists(name, key) {
log.Printf("Key already uploaded: %s", name)
return
// B) by key content
for _, upstreamKey := range upstreamKeys {
for _, lKey := range localKeyMap {
if lKey.Key == upstreamKey.Key {
selectedKey = lKey
log.Printf("Upstream has key: %s (found locally as %s)", upstreamKey.Name, lKey.File)
return
}
}
}
log.Printf("We have no common key with upstream")
// we need to upload a key
name, key := readPubKey(userArg)
log.Printf("Uploading key %s", name)
body := map[string]string{"name": name, "key": key}
......@@ -162,19 +254,18 @@ 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
}
func findServiceID() (serviceID int) {
// determined the id of the requested service
func findService(sid int) (service api.Service) {
serviceBytes, err := restCall("GET", "services", nil)
if err != nil {
log.Fatalf("Retrieving services: %s", err)
......@@ -182,53 +273,58 @@ func findServiceID() (serviceID int) {
var services = []api.Service{}
json.Unmarshal(serviceBytes, &services)
if *serviceName == "" {
fmt.Printf("Available services: (Services availability is based on your VO membership)\n\n")
if sid == 0 {
fmt.Fprintf(os.Stderr, "Available services: (Services availability is based on your VO membership)\n\n")
const (
minWidth = 0
padding = 3
tabWidth = 4
)
w := tabwriter.NewWriter(os.Stdout, minWidth, tabWidth, padding, ' ', 0)
fmt.Fprintf(w, "Service Name\tDescription\t\n")
w := tabwriter.NewWriter(os.Stderr, minWidth, tabWidth, padding, ' ', 0)
fmt.Fprintf(w, "ID\tService Name\tDescription\t\n")
for _, s := range services {
fmt.Fprintf(w, " - '%s'\t%s\t\n", s.Name, s.Description)
fmt.Fprintf(w, "%d\t%s\t%s\t\n", s.ID, s.Name, s.Description)
}
w.Flush()
fmt.Println("\nSpecify service name to use. See --help")
fmt.Fprintf(os.Stderr, "\nSpecify service ID to use. See --help\n")
os.Exit(1)
}
// find the service id
for _, s := range services {
if s.Name == *serviceName {
serviceID = s.ID
log.Printf("Selected service '%s' (id: %d)", *serviceName, serviceID)
if sid == s.ID {
service = s
log.Printf("Selected service: %s", service.Name)
return
}
}
if serviceID == 0 {
log.Fatalf("Service with name '%s' does not exist", *serviceName)
}
fmt.Fprintf(os.Stderr, "Service with ID '%d' does not exist\n", sid)
return
}
// pollDepState polls the api until the state reaches "deployed", "failed", or "questionnare"
func pollDepState(state api.DepState) api.DepState {
for state.State != "deployed" && state.State != "questionnaire" && state.State != "failed" {
interval := time.Second
for pollCount := 1; state.State != "deployed" && state.State != "questionnaire" && state.State != "failed"; pollCount++ {
// log old
log.Println("Deployment has state:", state.State)
// poll new
time.Sleep(time.Second)
time.Sleep(interval)
state = fetchDepState(state.ID)
if pollCount%5 == 0 {
interval *= 2
log.Printf("Throttling poll interval to %s", interval)
}
}
log.Println("Deployment has state:", state.State)
return state
}
func sshConnect(creds api.Credentials) {
func sshConnect(key api.SSHKey, creds api.Credentials) {
// use the credentials to access the service
if credsBytes, err := json.MarshalIndent(creds, "", " "); err == nil {
......@@ -246,32 +342,70 @@ func sshConnect(creds api.Credentials) {
log.Fatal("Invalid credentials for ssh. The service may not be suitable for ssh.")
}
var native = false
if *connectArg {
fmt.Fprintf(os.Stderr, "Deployment successful. Opening SSH connection.\n")
// only printing this to stdout
publicKeyFile := func(file string) ssh.AuthMethod {
buffer, err := ioutil.ReadFile(file)
if err != nil {
return nil
}
key, err := ssh.ParsePrivateKey(buffer)
if err != nil {
return nil
}
return ssh.PublicKeys(key)
}
sshConfig := &ssh.ClientConfig{
User: sshUser,
Auth: []ssh.AuthMethod{
publicKeyFile(strings.TrimSuffix(key.File, ".pub")),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
// use go native ssh?
if native {
var err error
var client *sshclient.Client
client, err = sshclient.DialWithKey(
sshHost+":22",
sshUser,
*pubKey,
)
// new connection
connection, err := ssh.Dial("tcp", sshHost+":22", sshConfig)
if err != nil {
log.Fatalf("Failed to dial: %s", err)
}
// new session
sshSession, err := connection.NewSession()
if err != nil {
log.Fatalf("Error dialing: %s", err)
log.Fatalf("Failed to create session: %s", err)
}
defer sshSession.Close()
// wire IO
sshSession.Stdin = os.Stdin
sshSession.Stdout = os.Stdout
sshSession.Stderr = os.Stderr
modes := ssh.TerminalModes{
ssh.ECHO: 0, // disable echoing
ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
}
if err := sshSession.RequestPty("vt100", 40, 80, modes); err != nil {
log.Fatal("request for pseudo terminal failed: ", err)
}
defer client.Close()
cmd := exec.Command("ssh", "-i", "/home/burgey/.ssh/id_room.hadiko", "room.hadiko")
cmd.Stdout = os.Stdout
err = cmd.Run()
if err := sshSession.Shell(); err != nil {
log.Fatalf("Shell: %s", err)
}
if err := sshSession.Wait(); err != nil {
log.Fatalf("Wait: %s", err)
}
} else {
fmt.Printf(
"\nDeployment successful. You can now ssh to the service using:\n ssh -i %s %s@%s'\n",
*pubKey, sshUser, sshHost,
)
fmt.Fprintf(os.Stderr, "FEUDAL> Deployment successful\n")
fmt.Fprintf(os.Stdout, " ssh -i %s %s@%s\n", strings.TrimSuffix(key.File, ".pub"), sshUser, sshHost)
}
}
func pubToPrivKey(pub string) string {
return strings.TrimSuffix(pub, ".pub")
}
// if the access token is a jwt we use the issuer contained in the information
......@@ -286,9 +420,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 {
......@@ -304,35 +438,45 @@ func inspectAccessToken(at string) (issuerURI string) {
}
func main() {
// Parse arguments
if _, err := app.Parse(os.Args[1:]); err != nil {
app.Usage(os.Args[1:])
fmt.Println("Error:", err)
fmt.Fprintln(os.Stderr, "Error:", err)
os.Exit(1)
}
// set verbose mode
if *verboseArg {
log.SetOutput(os.Stderr)
} else {
log.SetOutput(ioutil.Discard)
}
log.Printf("Using access token: %s", *accessToken)
if issuer := inspectAccessToken(*accessToken); issuer != "" {
*issuerURI = issuer
issuerURI = *issuerURIArg
if atIssuer := inspectAccessToken(*accessToken); atIssuer != "" {
issuerURI = atIssuer
}
log.Printf("Using issuer: %s", *issuerURI)
log.Printf("Using issuer: %s", issuerURI)
var serviceID = findServiceID()
// determine key
var publicKey = negotiatePublicKey(*pubKey)
log.Printf("Selected ssh key: %s", publicKey.File)
uploadPubKey()
// determine service
var service = findService(*serviceID)
// start the deployment
var deployment api.Deployment
body := "{\"state_target\":\"deployed\"}"
response, err := restCall("PATCH", fmt.Sprintf("deployment/service/%d", serviceID), strings.NewReader(body))
body := strings.NewReader("{\"state_target\":\"deployed\"}")
response, err := restCall("PATCH", fmt.Sprintf("deployment/service/%d", service.ID), body)
if err != nil {
log.Fatalf("Requesting deployment: %s", err)
}
var deployment api.Deployment
json.Unmarshal(response, &deployment)
if count := len(deployment.States); count != 1 {
fmt.Printf("%s", response)
log.Printf("Response: %s", response)
log.Fatalf("Got %d states, want 1", count)
}
......@@ -342,10 +486,10 @@ func main() {
switch state.State {
case "deployed":
sshConnect(state.Credentials)
sshConnect(publicKey, state.Credentials)
case "failed":
log.Fatal("Deployment failed. Please contact an administrator.")
default:
log.Fatalf("State %s: Not implemented", state.State)
log.Fatalf("Deployment has state %s: Not implemented", state.State)
}
}
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