diff --git a/.gitignore b/.gitignore index 1dcbc1b245a61925f2a7879ea2e89510e6faaf92..32dbba481b82ad0ce03caea4c4fc4e99270a13aa 100644 --- a/.gitignore +++ b/.gitignore @@ -125,4 +125,4 @@ broker/pkg discovery/pkg common/go/pkg authorizer/pkg - +asapo_tools/pkg diff --git a/CMakeLists.txt b/CMakeLists.txt index cd1e06fd386d49ccc06236e87a8bf592ee36d640..31dc962d885da0880e1b54fdfaabf7655074bfbc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -70,6 +70,8 @@ add_subdirectory(discovery) add_subdirectory(authorizer) +add_subdirectory(asapo_tools) + if(BUILD_INTEGRATION_TESTS) add_subdirectory(tests) diff --git a/asapo_tools/CMakeLists.txt b/asapo_tools/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..2a517df18b3ffb1417721dd797003b0fabb53fbf --- /dev/null +++ b/asapo_tools/CMakeLists.txt @@ -0,0 +1,36 @@ +set (TARGET_NAME asapo) + +if (NOT "$ENV{GOPATH}" STREQUAL "") + set(GOPATH $ENV{GOPATH}) +endif() + +if (NOT GOPATH) + message (FATAL_ERROR "GOPATH not set") +endif() + +message(STATUS "global gopath ${GOPATH}") + +IF(WIN32) + set (gopath "${GOPATH}\;${CMAKE_CURRENT_SOURCE_DIR}\;${CMAKE_SOURCE_DIR}/common/go") + set (exe_name "${TARGET_NAME}.exe") +ELSE() + set (gopath ${GOPATH}:${CMAKE_CURRENT_SOURCE_DIR}:${CMAKE_SOURCE_DIR}/common/go) + set (exe_name "${TARGET_NAME}") +ENDIF() + +include(testing_go) + +add_custom_target(asapo ALL + COMMAND ${CMAKE_COMMAND} -E env GOPATH=${gopath} + go build ${GO_OPTS} -o ${exe_name} asapo_tools/main + VERBATIM) +define_property(TARGET PROPERTY EXENAME + BRIEF_DOCS <executable name> + FULL_DOCS <full-doc>) + +set_target_properties(${TARGET_NAME} PROPERTIES EXENAME ${CMAKE_CURRENT_BINARY_DIR}/${exe_name}) + + +install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/${exe_name} DESTINATION bin) + +gotest(${TARGET_NAME} "./...") diff --git a/asapo_tools/src/asapo_tools/cli/cli.go b/asapo_tools/src/asapo_tools/cli/cli.go new file mode 100644 index 0000000000000000000000000000000000000000..031d25285cc96bae4d6dca8067cb2c4e8fd87728 --- /dev/null +++ b/asapo_tools/src/asapo_tools/cli/cli.go @@ -0,0 +1,59 @@ +// Package contains asapo commands that can be executed from command line. +// Every CommandXxxx function that is a member of a cmd struct processes asapo xxxx command +package cli + +import ( + "errors" + "flag" + "fmt" + "io" + "os" + "reflect" + "strings" +) + +var flHelp bool + +var outBuf io.Writer = os.Stdout + +func printHelp(f *flag.FlagSet) bool { + if flHelp { + f.Usage() + return true + } else { + return false + } +} + +// DoCommand takes command name as a parameter and executes corresponding to this name cmd method +func DoCommand(name string, args []string) error { + commandName := "Command" + strings.ToUpper(name[:1]) + strings.ToLower(name[1:]) + cmd := new(command) + + methodVal := reflect.ValueOf(cmd).MethodByName(commandName) + if !methodVal.IsValid() { + return errors.New("wrong asapo command: " + name + "\nType 'asapo --help'") + } + cmd.name = name + cmd.args = args + + method := methodVal.Interface().(func() error) + + return method() +} + +// PrintAllCommands prints all available commands (found wihtin methods of cmd) +func PrintAllCommands() { + fmt.Fprintln(outBuf, "\nCommands:") + cmd := new(command) + CmdType := reflect.TypeOf(cmd) + for i := 0; i < CmdType.NumMethod(); i++ { + methodVal := CmdType.Method(i) + if strings.HasPrefix(methodVal.Name, "Command") { + method := methodVal.Func.Interface().(func(*command) error) + cmd.name = strings.ToLower(methodVal.Name)[7:] + cmd.args = []string{"description"} + method(cmd) + } + } +} diff --git a/asapo_tools/src/asapo_tools/cli/command.go b/asapo_tools/src/asapo_tools/cli/command.go new file mode 100644 index 0000000000000000000000000000000000000000..b61171a1a94dc23e97eebd89f41d7d8bd635198b --- /dev/null +++ b/asapo_tools/src/asapo_tools/cli/command.go @@ -0,0 +1,40 @@ +package cli + +import ( + "errors" + "flag" + "fmt" +) + +// A command consists of a command name and arguments, passed to this command (all after asapo name ...) +type command struct { + name string + args []string +} + +// description prints description line and returns true if first command argument is "description". +func (cmd *command) description(d string) bool { + if len(cmd.args) == 1 && cmd.args[0] == "description" { + fmt.Fprintf(outBuf, " %-10s %s\n", cmd.name, d) + return true + } + return false +} + +func (cmd *command) errBadOptions(err string) error { + return errors.New("asapo " + cmd.name + ": " + err + "\nType 'asapo " + cmd.name + " --help'") +} + +// createDefaultFlagset creates new flagset and adds default help behaviour. +func (cmd *command) createDefaultFlagset(description, args string) *flag.FlagSet { + + flags := flag.NewFlagSet(cmd.name, flag.ExitOnError) + flags.BoolVar(&flHelp, "help", false, "Print usage") + flags.Usage = func() { + fmt.Fprintf(outBuf, "Usage:\t\nasapo %s "+args, cmd.name) + fmt.Fprintf(outBuf, "\n\n%s\n", description) + flags.PrintDefaults() + } + + return flags +} diff --git a/asapo_tools/src/asapo_tools/cli/command_test.go b/asapo_tools/src/asapo_tools/cli/command_test.go new file mode 100644 index 0000000000000000000000000000000000000000..c4e89f2239f69728ba190d4db6d7aa59011550b0 --- /dev/null +++ b/asapo_tools/src/asapo_tools/cli/command_test.go @@ -0,0 +1,34 @@ +package cli + +import ( + "bytes" + "testing" + "github.com/stretchr/testify/assert" +) + +var CommandTests = []struct { + cmd command + answer string +}{ + {command{"token", []string{"-secret", "secret_file", "beamtime"}}, "secret"}, + {command{"dummy", []string{"description"}}, "wrong"}, +} + +func TestCommand(t *testing.T) { + outBuf = new(bytes.Buffer) + + for _, test := range CommandTests { + outBuf.(*bytes.Buffer).Reset() + err := DoCommand(test.cmd.name, test.cmd.args) + assert.Contains(t, err.Error(), test.answer, "") + assert.NotNil(t, err, "Should be error") + + } + +} + +func TestPrintAllCommands(t *testing.T) { + outBuf = new(bytes.Buffer) + PrintAllCommands() + assert.Contains(t, outBuf.(*bytes.Buffer).String(), "token", "all commands must have token") +} diff --git a/asapo_tools/src/asapo_tools/cli/token.go b/asapo_tools/src/asapo_tools/cli/token.go new file mode 100644 index 0000000000000000000000000000000000000000..3f0ecfe7ccbd5b53d5a296e125058e5bd2c3e3fb --- /dev/null +++ b/asapo_tools/src/asapo_tools/cli/token.go @@ -0,0 +1,83 @@ +package cli + +import ( + "errors" + "os" + "fmt" + "asapo_common/utils" +) + +type tokenFlags struct { + BeamtimeID string + SecretFile string +} + +func generateToken(id string,secret string) string { + jwt := utils.NewJWTAuth(secret) + load := utils.JobClaim{id} + + var c utils.CustomClaims + c.ExtraClaims = &load + + token,err := jwt.GenerateToken(&c) + + if (err!=nil) { + fmt.Println(err.Error()) + } + return token +} + + +// GenerateToken generates token for workers +func (cmd *command) CommandToken() error { + + message_string := "Generate token" + + if cmd.description(message_string) { + return nil + } + + flags, err := cmd.parseTokenFlags(message_string) + if err != nil { + return err + } + + strings, err := utils.ReadStringsFromFile(flags.SecretFile) + if err !=nil { + return err + } + + + + fmt.Fprintf(outBuf, "%s\n", generateToken(flags.BeamtimeID,strings[0])) + + return nil +} + + +func (cmd *command) parseTokenFlags(message_string string) (tokenFlags, error) { + + var flags tokenFlags + flagset := cmd.createDefaultFlagset(message_string, "<beamtime id>") + flagset.StringVar(&flags.SecretFile, "secret", "", "path to file with secret") + + flagset.Parse(cmd.args) + + if printHelp(flagset) { + os.Exit(0) + } + + flags.BeamtimeID = flagset.Arg(0) + + if flags.BeamtimeID == "" { + return flags, errors.New("beamtime id missed ") + } + + if flags.SecretFile == "" { + return flags, errors.New("secret file missed ") + } + + + return flags, nil + +} diff --git a/asapo_tools/src/asapo_tools/cli/token_test.go b/asapo_tools/src/asapo_tools/cli/token_test.go new file mode 100644 index 0000000000000000000000000000000000000000..4149f9ae48b1b19b7110eff8c1ada26e955fac36 --- /dev/null +++ b/asapo_tools/src/asapo_tools/cli/token_test.go @@ -0,0 +1,39 @@ +package cli + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "bytes" + "io/ioutil" + "os" +) + +var tokenTests = []struct { + cmd command + answer string + msg string +}{ + {command{args: []string{"beamtime_id"}}, "secret", "no secret parameter"}, + {command{args: []string{"-secret","secret.tmp"}}, "beamtime id", "no file"}, + {command{args: []string{"-secret","not_existing_file","beamtime_id"}}, "not_existing_file", "no file"}, + {command{args: []string{"-secret","secret.tmp","beamtime_id"}}, "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + + "eyJFeHRyYUNsYWltcyI6eyJCZWFtdGltZUlkIjoiYmVhbXRpbWVfaWQifX0.OEYaJfOL6-0yY7B7zW73l372ZnR9GF2IUYMLuDQtmSo", "no file"}, +} + +func TestParseTokenFlags(t *testing.T) { + + ioutil.WriteFile("secret.tmp", []byte("secret"), 0644) + outBuf = new(bytes.Buffer) + for _, test := range tokenTests { + err := test.cmd.CommandToken() + if err == nil { + assert.Contains(t, outBuf.(*bytes.Buffer).String(), test.answer, test.msg) + } else { + assert.Contains(t, err.Error(), test.answer, test.msg) + } + + } + os.Remove("secret.tmp") + +} diff --git a/asapo_tools/src/asapo_tools/main/asapo.go b/asapo_tools/src/asapo_tools/main/asapo.go new file mode 100644 index 0000000000000000000000000000000000000000..e326889315ffd4f4c324a83bc646b79aa09acb92 --- /dev/null +++ b/asapo_tools/src/asapo_tools/main/asapo.go @@ -0,0 +1,33 @@ +package main + +import ( + "flag" + "fmt" + "os" + "asapo_tools/version" + "asapo_tools/cli" +) + +var ( + flHelp = flag.Bool("help", false, "Print usage") +) + +func main() { + + if ret := version.ShowVersion(os.Stdout, "asapo"); ret { + return + } + + flag.Parse() + + if *flHelp || flag.NArg() == 0 { + flag.Usage() + cli.PrintAllCommands() + return + } + + if err := cli.DoCommand(flag.Arg(0), flag.Args()[1:]); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/asapo_tools/src/asapo_tools/version/version.go b/asapo_tools/src/asapo_tools/version/version.go new file mode 100644 index 0000000000000000000000000000000000000000..e2427a17c6038e422fb6c7b102562dd254043f33 --- /dev/null +++ b/asapo_tools/src/asapo_tools/version/version.go @@ -0,0 +1,23 @@ +package version + +import ( + "flag" + "fmt" + "io" + "os" +) + +var version, buildTime, gitCommit, shortVersion string + +func ShowVersion(w io.Writer, name string) bool { + flags := flag.NewFlagSet("version", flag.ExitOnError) + flag.Bool("version", false, "Print version information") // to have it in main help + flVersion := flags.Bool("version", false, "Print version information") + flags.Bool("help", false, "Print usage") // define help flag but ignore it + flags.Parse(os.Args[1:]) + if *flVersion { + fmt.Fprintf(w, "%s version %s, build time %s\n", name, version, buildTime) + return true + } + return false +} diff --git a/asapo_tools/src/asapo_tools/version/version_lib.go.in b/asapo_tools/src/asapo_tools/version/version_lib.go.in new file mode 100644 index 0000000000000000000000000000000000000000..3fe1989dbbc62d3515284e43bd2c3e415e874471 --- /dev/null +++ b/asapo_tools/src/asapo_tools/version/version_lib.go.in @@ -0,0 +1,10 @@ +package version + +// Default build-time variable for library-import. +// This file is overridden on build with build-time informations. +func init(){ + gitCommit = "@VERSION_SHA1@" + version = "@VERSION@" + shortVersion = "@VERSION_SHORT@" + buildTime = "@TIMESTAMP@" +} diff --git a/common/go/src/asapo_common/utils/authorization.go b/common/go/src/asapo_common/utils/authorization.go new file mode 100644 index 0000000000000000000000000000000000000000..ff8f69d210c170bcad7bbf8add02421e110a7843 --- /dev/null +++ b/common/go/src/asapo_common/utils/authorization.go @@ -0,0 +1,172 @@ +package utils + +import ( + "errors" + "net/http" + "net/url" + "strings" + "context" + "github.com/dgrijalva/jwt-go" +) + +type AuthorizationRequest struct { + Token string + Command string + URL string +} + +type AuthorizationResponce struct { + Status int + StatusText string + UserName string + Token string + ValidityTime int +} + +type Auth interface { + GenerateToken(...interface{}) (string, error) + Name() string +} + + +func (a *JWTAuth) Name() string { + return "Bearer" +} + + +func stripURL(u *url.URL) string { + s := u.Path + u.RawQuery + s = strings.Replace(s, "/", "", -1) + s = strings.Replace(s, "?", "", -1) + return s + +} + +func SplitAuthToken(s string) (authType, token string, err error) { + keys := strings.Split(s, " ") + + if len(keys) != 2 { + err = errors.New("authorization error - wrong token") + return + } + + authType = keys[0] + token = keys[1] + return +} + +func ExtractAuthInfo(r *http.Request) (authType, token string, err error) { + + t := r.Header.Get("Authorization") + + if t != "" { + return SplitAuthToken(t) + } + + cookie, err := r.Cookie("Authorization") + if err == nil { + return SplitAuthToken(cookie.Value) + } + + err = errors.New("no authorization info") + return + +} + +type CustomClaims struct { + jwt.StandardClaims + ExtraClaims interface{} +} + +type JobClaim struct { + BeamtimeId string +} + +type JWTAuth struct { + Key string +} + +func NewJWTAuth(key string) *JWTAuth { + a := JWTAuth{key} + return &a +} + +func (t JWTAuth) GenerateToken(val ...interface{}) (string, error) { + if len(val) != 1 { + return "", errors.New("No claims") + } + claims, ok := val[0].(*CustomClaims) + if !ok { + return "", errors.New("Wrong claims") + } + +// if claims.Duration > 0 { +// claims.ExpiresAt = time.Now().Add(claims.Duration).Unix() +// } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err := token.SignedString([]byte(t.Key)) + + if err != nil { + return "", err + } + + return tokenString, nil +} + +func ProcessJWTAuth(fn http.HandlerFunc, key string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + authType, token, err := ExtractAuthInfo(r) + + if err != nil { + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + + ctx := r.Context() + + if authType == "Bearer" { + if claims, ok := CheckJWTToken(token, key); !ok { + http.Error(w, "Internal authorization error - tocken does not match", http.StatusUnauthorized) + return + } else { + ctx = context.WithValue(ctx, "JobClaim", claims) + } + } else { + http.Error(w, "Internal authorization error - wrong auth type", http.StatusUnauthorized) + return + } + fn(w, r.WithContext(ctx)) + } +} + +func CheckJWTToken(token, key string) (jwt.Claims, bool) { + + if token == "" { + return nil, false + } + + t, err := jwt.ParseWithClaims(token, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) { + return []byte(key), nil + }) + + if err == nil && t.Valid { + return t.Claims, true + } + + return nil, false +} + +func JobClaimFromContext(r *http.Request, val interface{}) error { + c := r.Context().Value("JobClaim") + + if c == nil { + return errors.New("Empty context") + } + + claim := c.(*CustomClaims) + + return MapToStruct(claim.ExtraClaims.(map[string]interface{}), val) +} + diff --git a/common/go/src/asapo_common/utils/helpers.go b/common/go/src/asapo_common/utils/helpers.go index 9f02ac4263ae8f69f0839bb3c204f2374bce4415..72b7afd835eacde88be72bbd9c370dbf47869c92 100644 --- a/common/go/src/asapo_common/utils/helpers.go +++ b/common/go/src/asapo_common/utils/helpers.go @@ -47,4 +47,16 @@ func ReadStringsFromFile(fname string) ([]string, error) { lines := strings.Split(string(content), "\n") return lines,nil -} \ No newline at end of file +} + +func MapToStruct(m map[string]interface{}, val interface{}) error { + tmp, err := json.Marshal(m) + if err != nil { + return err + } + err = json.Unmarshal(tmp, val) + if err != nil { + return err + } + return nil +}