diff --git a/auth.go b/authenticate.go similarity index 59% rename from auth.go rename to authenticate.go index ebedba3..eecff35 100644 --- a/auth.go +++ b/authenticate.go @@ -5,25 +5,13 @@ import ( "encoding/json" "flag" "fmt" + "libshared" "log" "net/http" "os" + "path/filepath" ) -/* -func authMain(args []string) { - authCmd := flag.NewFlagSet("auth", flag.ExitOnError) - token := authCmd.String("t", "", "Auth token") - verbose := authCmd.Bool("v", false, "Verbose output") - - authCmd.Parse(args) - - fmt.Println("Auth command") - fmt.Println("Token:", *token) - fmt.Println("Verbose:", *verbose) -} -*/ - var authCmd *flag.FlagSet type AuthenticateRequest struct { @@ -32,53 +20,34 @@ type AuthenticateRequest struct { Password string `json:"password"` } +type AuthenticateResponse struct { + Token string `json:"token"` +} + func authLocalGetToken() { } func authLocalAuthenticate(args []string) { - - var authSource string var authUsername string var authPassword string var authAccountid string var outputFormat string + var saveProfile string - authCmd.StringVar(&authSource, "s", "", "Source (required)") - authCmd.StringVar(&authSource, "source", "", "Source (required)") + authCmd.StringVar(&saveProfile, "save-profile", "", "Save authentication profile with given name") authCmd.StringVar(&authUsername, "u", "", "Username (required)") authCmd.StringVar(&authUsername, "username", "", "Username (required)") authCmd.StringVar(&authPassword, "p", "", "Password (required)") authCmd.StringVar(&authPassword, "password", "", "Password (required)") authCmd.StringVar(&authAccountid, "a", "", "Account ID (required)") authCmd.StringVar(&authAccountid, "account", "", "Account ID (required)") - authCmd.StringVar(&outputFormat, "o", "text", "Output format (text or json)") + authCmd.StringVar(&outputFormat, "o", "json", "Output format (text or json)") - // verbose := authCmd.Bool("v", false, "Verbose output") - - outputFormat = "text" + outputFormat = "json" authCmd.Parse(args) - if authSource == "" { - fmt.Println("Error: either -s or --source is required") - //authCmd.Usage() - os.Exit(1) - } - - // Enforce required flag - if authSource == "" { - fmt.Println("Error: either -s or --source is required") - //authCmd.Usage() - os.Exit(1) - } - - if authSource != "local" { - fmt.Println("Error: invalid source. Allowed values: local") - //authCmd.Usage() - os.Exit(1) - } - if authAccountid == "" { fmt.Print("Account ID: ") fmt.Scanln(&authAccountid) @@ -91,6 +60,11 @@ func authLocalAuthenticate(args []string) { if authPassword == "" { fmt.Print("Password: ") fmt.Scanln(&authPassword) + + } + + if (outputFormat != "json") && (outputFormat != "text") && (outputFormat != "silent") { + log.Fatal("Invalid output format. Use 'json', 'text', or 'silent'.") } var authenticaterequest AuthenticateRequest @@ -125,30 +99,58 @@ func authLocalAuthenticate(args []string) { log.Fatal("Authentication failed with status:", resp.Status) } - var result map[string]interface{} - err = json.NewDecoder(resp.Body).Decode(&result) + apiresponse := libshared.APIResponse[AuthenticateResponse]{} + + // var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&apiresponse) if err != nil { log.Fatal("Error decoding response:", err) } - if outputFormat == "json" { - jsonOutput, err := json.MarshalIndent(result, "", " ") + if saveProfile != "" { + + home, err := os.UserHomeDir() + if err != nil { + panic(err) + } + + dir := filepath.Dir(home + "/.pcloud/identities/" + saveProfile + ".json") + err = os.MkdirAll(dir, 0700) + if err != nil { + log.Fatal("Error creating directory:", err) + } + + profileData, err := json.MarshalIndent(apiresponse.Content, "", "\t") + if err != nil { + log.Fatal("Error encoding profile JSON:", err) + } + + err = os.Remove(home + "/.pcloud/identities/" + saveProfile + ".json") + if err != nil && !os.IsNotExist(err) { + log.Fatal("Error removing profile:", err) + } + + err = os.WriteFile(home+"/.pcloud/identities/"+saveProfile+".json", profileData, 0600) + if err != nil { + log.Fatal("Error saving profile:", err) + } + } + + switch outputFormat { + case "json": + jsonOutput, err := json.MarshalIndent(apiresponse.Content, "", "\t") if err != nil { log.Fatal("Error encoding JSON output:", err) } fmt.Println(string(jsonOutput)) - return - } else { - for key, value := range result { - fmt.Printf("%s: %v\n", key, value) - } - } + case "text": + fmt.Printf("Status: %s\nMessage: %s\n", apiresponse.Status, apiresponse.Message) + case "silent": + } } func authenticateMain(args []string) { authCmd = flag.NewFlagSet("auth", flag.ExitOnError) - authLocalAuthenticate(args) - //fmt.Println("Verbose:", *verbose) } diff --git a/go.mod b/go.mod index 701260f..0f6a016 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,7 @@ module upc go 1.25.0 + +require golang.org/x/term v0.41.0 + +require golang.org/x/sys v0.42.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..91a25f5 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= diff --git a/main.go b/main.go index 3691c6e..63a5fcb 100644 --- a/main.go +++ b/main.go @@ -16,17 +16,20 @@ type Command struct { var commands = []Command{ { - Names: []string{"login", "l"}, - Description: "Log into the system", - Handler: func(args []string) { - fmt.Println("Executing login with args:", args) - }, + Names: []string{"role"}, + Description: "Role Management", + Handler: roleMain, }, { - Names: []string{"auth", "authenticate"}, - Description: "Authenticate with a token", + Names: []string{"auth", "authorization"}, + Description: "Role Management", Handler: authenticateMain, }, + { + Names: []string{"policy"}, + Description: "Policy Management", + Handler: policyMain, + }, } func printMainUsage() { @@ -43,7 +46,7 @@ func formatNames(names []string) string { return strings.Join(names, ", ") } -func findCommand(name string) *Command { +func findCommand(name string, commands []Command) *Command { for _, cmd := range commands { for _, n := range cmd.Names { if n == name { @@ -64,7 +67,7 @@ func main() { subcommand := os.Args[1] - cmd := findCommand(subcommand) + cmd := findCommand(subcommand, commands) if cmd == nil { fmt.Println("Error: unknown command:", subcommand) printMainUsage() diff --git a/policy.go b/policy.go new file mode 100644 index 0000000..fcf2c02 --- /dev/null +++ b/policy.go @@ -0,0 +1,204 @@ +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "net/http" + "os" +) + +var policyCmd *flag.FlagSet + +var policyCommands = []Command{ + { + Names: []string{"create-policy"}, + Description: "Create Policy", + Handler: policyCreate, + }, + { + Names: []string{"list-policies"}, + Description: "List Policies", + Handler: policyCreate, + }, +} + +// Condition represents each item in the "conditions" array +type Condition struct { + StatementID string `json:"statementid"` + Principal []string `json:"principals"` + Actions []string `json:"actions"` + Source []string `json:"source"` + Effect string `json:"effect"` + Operator string `json:"operator"` +} + +type Policy struct { + Name string `json:"name"` + Description string `json:"description"` + Conditions []Condition `json:"conditions"` +} + +type CreatePolicyRequest struct { + Name string `json:"name"` + Description string `json:"description"` + PolicyDocument Policy `json:"PolicyDocument"` +} + +type CreatePolicyResponse struct { + PolicyID string `json:"policy_id"` +} + +type ProfileToken struct { + Token string `json:"token"` +} + +type IdentityToken struct { + Token string `json:"token"` +} + +func policyCreate(args []string) { + policyCmd := flag.NewFlagSet("create-policy", flag.ExitOnError) + + var policyname string + var useprofile string + var policyjson string + var policyfile string + var policyDescription string + var createpolicyrequest CreatePolicyRequest + + var normalizedDocument string + + policyCmd.StringVar(&policyname, "name", "", "Policy Name (required)") + policyCmd.StringVar(&policyDescription, "description", "", "Policy Description") + policyCmd.StringVar(&useprofile, "profile", "", "Profile") + policyCmd.StringVar(&policyjson, "policy-json", "", "Policy JSON") + policyCmd.StringVar(&policyfile, "policy-file", "", "JSON Policy File") + + policyCmd.Parse(args) + + if policyname == "" { + fmt.Println("Error: either -n or --name is required") + os.Exit(1) + } + + if useprofile == "" { + fmt.Println("Error: either -profile or --profile is required") + os.Exit(1) + } + + if policyjson == "" && policyfile == "" { + fmt.Println("Error: either -p/--policy-json or -f/--policy-file is required") + os.Exit(1) + } + + if policyjson != "" && policyfile != "" { + fmt.Println("Error: only one of -p/--policy-json or -f/--policy-file can be provided") + os.Exit(1) + } + + // Open Profile file + home, err := os.UserHomeDir() + if err != nil { + panic(err) + } + profileData, err := os.ReadFile(home + "/.pcloud/profiles/" + useprofile + ".json") + if err != nil { + fmt.Printf("Error opening profile file: %v\n", err) + os.Exit(1) + } + + fmt.Println("Length is", len(profileData)) + + if len(profileData) == 0 { + fmt.Printf("Profile file is empty: %s\n", home+"/.pcloud/profiles/"+useprofile+".json") + os.Exit(1) + } + + var profileToken ProfileToken + err = json.Unmarshal(profileData, &profileToken) + if err != nil { + fmt.Printf("Error reading profile file: %v\n", err) + os.Exit(1) + } + + fmt.Println("Profile Token: ", profileToken.Token) + + if policyfile != "" { + content, err := os.ReadFile(policyfile) + if err != nil { + fmt.Printf("Error reading policy file: %v\n", err) + os.Exit(1) + } + normalizedDocument = string(content) + } else if policyjson != "" { + normalizedDocument = policyjson + } + + // Validate JSON + if !json.Valid([]byte(normalizedDocument)) { + fmt.Println("Error: invalid JSON for policy document") + os.Exit(1) + } + + createpolicyrequest.Name = policyname + + err = json.Unmarshal([]byte(normalizedDocument), &createpolicyrequest.PolicyDocument) + if err != nil { + fmt.Printf("Error reading policy document: %v\n", err) + os.Exit(1) + } + + policyrequestData, err := json.Marshal(createpolicyrequest) + if err != nil { + fmt.Printf("Error encoding policy request: %v\n", err) + os.Exit(1) + } + + fmt.Println("Policy Name:", createpolicyrequest.Name) + fmt.Println("Policy JSON:", createpolicyrequest.PolicyDocument) + + apiendpoint := endpoint + "/policy/create-policy" + fmt.Println(apiendpoint) + + req, err := http.NewRequest("POST", apiendpoint, bytes.NewBuffer(policyrequestData)) + if err != nil { + panic(err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+profileToken.Token) + fmt.Println("Using token:", profileToken.Token) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + panic(err) + } + + defer resp.Body.Close() + + fmt.Println("Response status:", resp.Status, resp.Body) + +} + +func policyList(args []string) { +} + +func policyMain(args []string) { + fmt.Println("Policy Main") + if len(args) < 1 { + fmt.Println("Error: subcommand is required") + os.Exit(1) + } + subcommand := args[0] + + cmd := findCommand(subcommand, policyCommands) + if cmd == nil { + fmt.Println("Error: unknown command:", subcommand) + os.Exit(1) + } + cmd.Handler(args[1:]) + +} diff --git a/request.go b/request.go new file mode 100644 index 0000000..0b70ead --- /dev/null +++ b/request.go @@ -0,0 +1,5 @@ +package main + +func getReq() { + +} diff --git a/role.go b/role.go new file mode 100644 index 0000000..eb44727 --- /dev/null +++ b/role.go @@ -0,0 +1,232 @@ +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "libshared" + "net/http" + "os" +) + +type AssumeRoleRequest struct { + Rolename string `json:"rolename"` +} + +type AssumeRoleResponse struct { + Token string `json:"token"` +} + +var roleattachpolicyCmd *flag.FlagSet + +var roleCommands = []Command{ + { + Names: []string{"attach-policy"}, + Description: "Role Management", + Handler: roleAttachPolicy, + }, + { + Names: []string{"detach-policy"}, + Description: "Role Management", + Handler: roleDetachPolicy, + }, + { + Names: []string{"assume-role"}, + Description: "Role Management", + Handler: assumeRole, + }, +} + +func assumeRole(args []string) { + fmt.Println("I assume something") + + var targetRole string + var useIdentity string + roleAssumeRoleCmd := flag.NewFlagSet("assume-role", flag.ExitOnError) + roleAssumeRoleCmd.StringVar(&targetRole, "r", "", "Target role (required)") + roleAssumeRoleCmd.StringVar(&targetRole, "role", "", "Target role (required)") + roleAssumeRoleCmd.StringVar(&useIdentity, "identity", "", "Identity to use (required)") + + roleAssumeRoleCmd.Parse(args) + + if targetRole == "" { + fmt.Println("Error: either -r or --role is required") + os.Exit(1) + } + if useIdentity == "" { + fmt.Println("Error: either --identity is required") + os.Exit(1) + } + + identityData, err := os.ReadFile("/home/farhan/.pcloud/identities/" + useIdentity + ".json") + if err != nil { + fmt.Printf("Error opening identity file: %v\n", err) + os.Exit(1) + } + + var identityToken IdentityToken + err = json.Unmarshal(identityData, &identityToken) + if err != nil { + fmt.Printf("Error reading identity file: %v\n", err) + os.Exit(1) + } + + apiendpoint := endpoint + "/role/assume-role" + + var assumerolerequest AssumeRoleRequest + assumerolerequest.Rolename = targetRole + + jsonData, err := json.Marshal(assumerolerequest) + if err != nil { + fmt.Println("Error marshaling JSON:", err) + return + } + + req, err := http.NewRequest("POST", apiendpoint, bytes.NewBuffer(jsonData)) + if err != nil { + panic(err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+identityToken.Token) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + panic(err) + } + defer resp.Body.Close() + + // var assumeRoleResponse AssumeRoleResponse + + apiresponse := libshared.APIResponse[AssumeRoleResponse]{} + + if err := json.NewDecoder(resp.Body).Decode(&apiresponse); err != nil { + fmt.Printf("Error reading response: %v\n", err) + os.Exit(1) + } + + // fmt.Println("Response status:", resp.Status) + fmt.Println("Assumed role token:", apiresponse.Content.Token) + + // fmt.Printf("Attaching policy '%s' to role '%s'\n", , targetRole) + + fmt.Println("Response status:", resp.Status) + fmt.Printf("Assuming role '%s'\n", targetRole) + fmt.Println("Using identity", resp.Body) + + // Write apiresponse.Content to ~/.pcloud/roles/assumed-.json + home, err := os.UserHomeDir() + if err != nil { + panic(err) + } + + dir := fmt.Sprintf("%s/.pcloud/roles", home) + err = os.MkdirAll(dir, 0700) + if err != nil { + fmt.Printf("Error creating directory: %v\n", err) + os.Exit(1) + } + + roleData, err := json.MarshalIndent(apiresponse.Content, "", "\t") + if err != nil { + fmt.Printf("Error marshaling role data: %v\n", err) + os.Exit(1) + } + + roleFile := fmt.Sprintf("%s/%s.json", dir, targetRole) + err = os.WriteFile(roleFile, roleData, 0600) + if err != nil { + fmt.Printf("Error writing role file: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Assumed role token saved to %s\n", roleFile) + +} + +func roleAttachPolicy(args []string) { + roleattachpolicyCmd := flag.NewFlagSet("attach-policy", flag.ExitOnError) + + //var policyToAttach string + var targetRole string + var useIdentity string + + roleattachpolicyCmd.StringVar(&targetRole, "role", "", "Target role (required)") + roleattachpolicyCmd.StringVar(&useIdentity, "identity", "", "Identity to use (default: default)") + + roleattachpolicyCmd.Parse(args) + + if targetRole == "" { + fmt.Println("Error: either --role is required") + os.Exit(1) + } + if useIdentity == "" { + fmt.Println("Error: either --identity is required") + os.Exit(1) + } + + // Open Profile file + home, err := os.UserHomeDir() + if err != nil { + panic(err) + } + profileData, err := os.ReadFile(home + "/.pcloud/identities/" + useIdentity + ".json") + if err != nil { + fmt.Printf("Error opening profile file: %v\n", err) + os.Exit(1) + } + + // fmt.Println("Length is", len(profileData)) + + if len(profileData) == 0 { + fmt.Printf("Profile file is empty: %s\n", home+"/.pcloud/identities/"+useIdentity+".json") + os.Exit(1) + } + + var profileToken ProfileToken + err = json.Unmarshal(profileData, &profileToken) + if err != nil { + fmt.Printf("Error reading profile file: %v\n", err) + os.Exit(1) + } + + fmt.Println("Profile Token: ", profileToken.Token) + + apiendpoint := endpoint + "/role/attach-policy" + + req, err := http.NewRequest("POST", apiendpoint, nil) //, bytes.NewBuffer(jsonData)) + if err != nil { + panic(err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer YOUR_TOKEN_HERE") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + panic(err) + } + defer resp.Body.Close() +} + +func roleDetachPolicy(args []string) { + fmt.Println("I detach something") +} + +func roleMain(args []string) { + if len(args) < 1 { + fmt.Println("Error: subcommand is required") + os.Exit(1) + } + subcommand := args[0] + + cmd := findCommand(subcommand, roleCommands) + if cmd == nil { + fmt.Println("Error: unknown command:", subcommand) + os.Exit(1) + } + + cmd.Handler(args[1:]) + +}