diff --git a/.env_example b/.env_example index ac9eadf..bcc3c50 100644 --- a/.env_example +++ b/.env_example @@ -1,6 +1,9 @@ -NEO4J_URL="neo4j://localhost:7687" -PASSWORD=IAMmeIAMme!1! -NEO4J_AUTH="neo4j/${PASSWORD}" +NEO4J_URL=neo4j://localhost:7687 +NEO4J_USER=neo4j +NEO4J_PASS=IAMmeIAMme!1! +NEO4J_AUTH="${NEO4J_USER}/${NEO4J_PASS}" NEO4J_server_memory_heap_initial__size=12G NEO4J_server_memory_heap_max__size=16G -NEO4J_server_memory_pagecache_size=12G \ No newline at end of file +NEO4J_server_memory_pagecache_size=12G +OKTA_CLIENT_ORGURL=yourtenant.okta.com +OKTA_CLIENT_TOKEN="00SOMETHINGWRONG" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1c4a702..4c507b6 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ dist iamme* IAMme-IAMme* +*.sarif \ No newline at end of file diff --git a/cmd/dump.go b/cmd/dump.go new file mode 100644 index 0000000..555e982 --- /dev/null +++ b/cmd/dump.go @@ -0,0 +1,20 @@ +package cmd + +import ( + "IAMme-IAMme/pkg/app" + + "github.com/spf13/cobra" +) + +var usersCmd = &cobra.Command{ + Use: "dump", + Short: "Fetch Okta info and store them in Neo4j", + Run: func(cmd *cobra.Command, args []string) { + oktaNeo4jApp := app.NewOktaNeo4jApp(clients.okta, clients.neo4j) + oktaNeo4jApp.Dump() + }, +} + +func init() { + rootCmd.AddCommand(usersCmd) +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..d1e9909 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,30 @@ +package cmd + +import ( + "IAMme-IAMme/pkg/infra/neo4j" + "IAMme-IAMme/pkg/infra/okta" + "fmt" + + "github.com/spf13/cobra" +) + +type clients_type struct { + okta okta.OktaClient + neo4j neo4j.Neo4jClient +} + +var clients *clients_type +var rootCmd = &cobra.Command{ + Use: "iamme-iamme", + Short: "A CLI tool to interact with Okta and Neo4j", +} + +func Execute(oktaClient okta.OktaClient, neo4jClient neo4j.Neo4jClient) { + clients = &clients_type{ + oktaClient, + neo4jClient, + } + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + } +} diff --git a/go.mod b/go.mod index e7110c5..e1a85f7 100644 --- a/go.mod +++ b/go.mod @@ -2,14 +2,23 @@ module IAMme-IAMme go 1.20 -require github.com/okta/okta-sdk-golang/v2 v2.20.0 +require ( + github.com/joho/godotenv v1.5.1 + github.com/neo4j/neo4j-go-driver/v5 v5.14.0 + github.com/okta/okta-sdk-golang/v2 v2.20.0 + github.com/spf13/cobra v1.8.0 +) require ( - github.com/BurntSushi/toml v1.1.0 // indirect - github.com/cenkalti/backoff/v4 v4.1.3 // indirect - github.com/go-jose/go-jose/v3 v3.0.0 // indirect + github.com/BurntSushi/toml v1.3.2 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/go-jose/go-jose/v3 v3.0.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kelseyhightower/envconfig v1.4.0 // indirect + github.com/kr/pretty v0.1.0 // indirect github.com/patrickmn/go-cache v0.0.0-20180815053127-5633e0862627 // indirect - golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/crypto v0.15.0 // indirect + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 96fbe90..72ecc4c 100644 --- a/go.sum +++ b/go.sum @@ -1,21 +1,44 @@ github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= +github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= +github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/neo4j/neo4j-go-driver/v5 v5.14.0 h1:5x3vD4HkXQIktlG63jSG8v9iweGjmObIPU7Y9U0ThUI= +github.com/neo4j/neo4j-go-driver/v5 v5.14.0/go.mod h1:Vff8OwT7QpLm7L2yYr85XNWe9Rbqlbeb9asNXJTHO4k= github.com/okta/okta-sdk-golang/v2 v2.20.0 h1:EDKM+uOPfihOMNwgHMdno+NAsIfyXkVnoFAYVPay0YU= github.com/okta/okta-sdk-golang/v2 v2.20.0/go.mod h1:FMy5hN5G8Rd/VoS0XrfyPPhIfOVo78ZK7lvwiQRS2+U= github.com/patrickmn/go-cache v0.0.0-20180815053127-5633e0862627 h1:pSCLCl6joCFRnjpeojzOpEYs4q7Vditq8fySFG5ap3Y= github.com/patrickmn/go-cache v0.0.0-20180815053127-5633e0862627/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= @@ -23,13 +46,16 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= +golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 9205e8b..2117178 100644 --- a/main.go +++ b/main.go @@ -1,9 +1,18 @@ package main import ( - "fmt" + "IAMme-IAMme/cmd" + "IAMme-IAMme/pkg/infra/neo4j" + "IAMme-IAMme/pkg/infra/okta" + "log" + + "github.com/joho/godotenv" ) func main() { - fmt.Println("IAMme IAMme") + envFile, err := godotenv.Read(".env") + if err != nil { + log.Fatalln(err.Error()) + } + cmd.Execute(okta.NewOktaClient(envFile["OKTA_CLIENT_ORGURL"], envFile["OKTA_CLIENT_TOKEN"]), neo4j.NewNeo4jClient(envFile["NEO4J_URL"], envFile["NEO4J_USER"], envFile["NEO4J_PASS"])) } diff --git a/pkg/app/okta_neo4j.go b/pkg/app/okta_neo4j.go new file mode 100644 index 0000000..f9a7d1b --- /dev/null +++ b/pkg/app/okta_neo4j.go @@ -0,0 +1,95 @@ +package app + +import ( + "IAMme-IAMme/pkg/infra/neo4j" + "IAMme-IAMme/pkg/infra/okta" + "context" + "log" + "strings" + + neo4jSdk "github.com/neo4j/neo4j-go-driver/v5/neo4j" +) + +type OktaNeo4jApp interface { + Dump() +} + +func NewOktaNeo4jApp(oktaClient okta.OktaClient, neo4jClient neo4j.Neo4jClient) OktaNeo4jApp { + return &oktaNeo4jApp{ + oktaClient: oktaClient, + neo4jClient: neo4jClient, + } +} + +type oktaNeo4jApp struct { + oktaClient okta.OktaClient + neo4jClient neo4j.Neo4jClient +} + +func (a *oktaNeo4jApp) Dump() { + users, err := a.oktaClient.GetUsers() + if err != nil { + log.Println(err.Error()) + } + + userParams := make([]map[string]interface{}, 0) + for _, user := range users { + userParams = append(userParams, map[string]interface{}{ + "userId": user.Id, + "status": user.Status, + "firstName": (*user.Profile)["firstName"], + "lastName": (*user.Profile)["lastName"], + }) + } + + session := a.neo4jClient.Connect() + ctx := context.TODO() + query := buildDynamicQuery(userParams) + _, err = session.ExecuteWrite(ctx, func(tx neo4jSdk.ManagedTransaction) (interface{}, error) { + _, err := tx.Run(ctx, query, map[string]interface{}{ + "userParams": userParams, + }) + + if err != nil { + panic(err) + } + return nil, err + }) + if err != nil { + log.Fatalln(err.Error()) + } +} + +// TODO: this is ugly AF +func buildDynamicQuery(userParams []map[string]interface{}) string { + var queryBuilder strings.Builder + + queryBuilder.WriteString("UNWIND $userParams as user\n") + queryBuilder.WriteString("CREATE (u:User {\n") + + fields := userParams[0] + + i := 0 + for field := range fields { + queryBuilder.WriteString(fieldKeyToCypherProperty(field) + ": user." + fieldKeyToCypherProperty(field)) + i++ + if i < len(fields) { + queryBuilder.WriteString(",\n") + } + } + + queryBuilder.WriteString("\n})\n") + queryBuilder.WriteString("RETURN u\n") + + return queryBuilder.String() +} + +// TODO: this is ugly AF +func fieldKeyToCypherProperty(key interface{}) string { + keyStr, ok := key.(string) + if !ok { + panic("Invalid field key") + } + + return keyStr +} diff --git a/pkg/infra/neo4j/neo4j_client.go b/pkg/infra/neo4j/neo4j_client.go new file mode 100644 index 0000000..f9302ad --- /dev/null +++ b/pkg/infra/neo4j/neo4j_client.go @@ -0,0 +1,49 @@ +package neo4j + +import ( + "context" + "log" + + "github.com/neo4j/neo4j-go-driver/v5/neo4j" +) + +// Neo4jClient is an interface for interacting with the Neo4j database. +type Neo4jClient interface { + Connect() neo4j.SessionWithContext + Close() error +} + +// Session is an interface for a Neo4j database session. +type Session interface { + Run(cypher string, params map[string]interface{}) Result + Close() error +} + +// Result is an interface for a Neo4j query result. +type Result interface { + Consume() (int, error) +} + +type neo4jClient struct { + driver neo4j.DriverWithContext +} + +func NewNeo4jClient(dbUri, username, password string) Neo4jClient { + driver, err := neo4j.NewDriverWithContext(dbUri, neo4j.BasicAuth(username, password, "")) + if err != nil { + log.Fatalln("Invalid Neo4j login", err.Error()) + } + return &neo4jClient{ + driver: driver, + } +} + +func (c *neo4jClient) Connect() neo4j.SessionWithContext { + return c.driver.NewSession(context.TODO(), neo4j.SessionConfig{ + AccessMode: neo4j.AccessModeWrite, + }) +} + +func (c *neo4jClient) Close() error { + return c.driver.Close(context.TODO()) +} diff --git a/pkg/infra/okta/okta_client.go b/pkg/infra/okta/okta_client.go new file mode 100644 index 0000000..41ef416 --- /dev/null +++ b/pkg/infra/okta/okta_client.go @@ -0,0 +1,47 @@ +package okta + +import ( + "context" + "fmt" + "log" + + "github.com/okta/okta-sdk-golang/v2/okta" +) + +// OktaClient is an interface for interacting with Okta resources. +type OktaClient interface { + GetUsers() ([]*okta.User, error) + GetGroups() ([]*okta.Group, error) +} + +type oktaClient struct { + oktaClient *okta.Client + context context.Context +} + +func NewOktaClient(orgUrl, apiKey string) OktaClient { + ctx, client, err := okta.NewClient(context.Background(), okta.WithOrgUrl(fmt.Sprintf("https://%s", orgUrl)), okta.WithToken(apiKey)) + if err != nil { + log.Fatalln("Invalid Okta login", err.Error()) + } + return &oktaClient{ + oktaClient: client, + context: ctx, + } +} + +func (c *oktaClient) GetUsers() ([]*okta.User, error) { + users, _, err := c.oktaClient.User.ListUsers(context.TODO(), nil) + if err != nil { + return nil, err + } + return users, nil +} + +func (c *oktaClient) GetGroups() ([]*okta.Group, error) { + groups, _, err := c.oktaClient.Group.ListGroups(context.TODO(), nil) + if err != nil { + return nil, err + } + return groups, nil +}