Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

135 add stack account self service features #150

Open
wants to merge 30 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
7bcfc67
created command
c0untingNumbers Nov 8, 2024
6e0890f
created command runners
c0untingNumbers Nov 8, 2024
bb45fda
Fixed comments
c0untingNumbers Nov 8, 2024
6862b84
Added GetEmail
c0untingNumbers Nov 8, 2024
225b3ad
Openstack self-service
c0untingNumbers Nov 8, 2024
29a82fb
enabled command
c0untingNumbers Nov 8, 2024
e856e45
changed command name
c0untingNumbers Nov 8, 2024
5b44147
added missing slashes for filepaths
c0untingNumbers Nov 8, 2024
5f35392
Added function to source on openrc.sh
c0untingNumbers Nov 8, 2024
f0a1ad0
Added sh -c
c0untingNumbers Nov 8, 2024
4cde432
sh -> bash
c0untingNumbers Nov 8, 2024
07cdb33
Added debugcreate
c0untingNumbers Nov 8, 2024
cf86b07
Changed run to start and wait
c0untingNumbers Nov 8, 2024
8ed659a
removed redundant debugging
c0untingNumbers Nov 8, 2024
fc57194
debug the commands
c0untingNumbers Nov 8, 2024
f4229fa
Added values for openstack environment variables and paths of scripts
c0untingNumbers Nov 8, 2024
5c4f8ef
deleted sourceRc and just did setenvs for openstack variables
c0untingNumbers Nov 8, 2024
58c2ca8
refactored with updated helpers
c0untingNumbers Nov 8, 2024
46dd69e
Fixed typo, was running wrong script
c0untingNumbers Nov 9, 2024
3300f7e
Debugging messages
c0untingNumbers Nov 9, 2024
fde6948
Converted output to string for comparison
c0untingNumbers Nov 9, 2024
e3e1502
debug
c0untingNumbers Nov 9, 2024
f2375f4
Trim whitespace of output
c0untingNumbers Nov 9, 2024
cf6a694
Fixed reset
c0untingNumbers Nov 10, 2024
1d44e96
Removed debug code
c0untingNumbers Nov 10, 2024
d73784e
Created InitialMessage and UpdateMessage
c0untingNumbers Nov 10, 2024
df548d2
Refactored with new helpers and cleaned up
c0untingNumbers Nov 10, 2024
c0a9220
Added missing return var
c0untingNumbers Nov 10, 2024
865e84b
Refactored and combined into one file
c0untingNumbers Nov 10, 2024
45f6ea8
Merge branch 'main' into 135-add-stack-account-self-service-features
c0untingNumbers Nov 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions commands/enabled.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func populateSlashCommands(ctx ddtrace.SpanContext) {
SlashCommands["dquery"] = slash.DQuery
SlashCommands["attendance"] = slash.Attendance
SlashCommands["attendanceof"] = slash.Attendanceof
SlashCommands["openstack"] = slash.Openstack
}

// populateHandlers populates the Handlers map with all of the handlers
Expand Down
280 changes: 280 additions & 0 deletions commands/slash/openstack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
package slash

import (
"bytes"
"fmt"
"os"
"os/exec"
"strings"

"github.com/bwmarrin/discordgo"
"github.com/ritsec/ops-bot-iii/commands/slash/permission"
"github.com/ritsec/ops-bot-iii/config"
"github.com/ritsec/ops-bot-iii/data"
"github.com/ritsec/ops-bot-iii/helpers"
"github.com/ritsec/ops-bot-iii/logging"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
)

var (
// Environment variables required for openstack CLI
OS_AUTH_URL string = config.GetString("openstack.ENV.OS_AUTH_URL")
OS_PROJECT_ID string = config.GetString("openstack.ENV.OS_PROJECT_ID")
OS_PROJECT_NAME string = config.GetString("openstack.ENV.OS_PROJECT_NAME")
OS_USER_DOMAIN_NAME string = config.GetString("openstack.ENV.OS_USER_DOMAIN_NAME")
OS_PROJECT_DOMAIN_ID string = config.GetString("openstack.ENV.OS_PROJECT_DOMAIN_ID")
OS_USERNAME string = config.GetString("openstack.ENV.OS_USERNAME")
OS_PASSWORD string = config.GetString("openstack.ENV.OS_PASSWORD")
OS_REGION_NAME string = config.GetString("openstack.ENV.OS_REGION_NAME")
OS_INTERFACE string = config.GetString("openstack.ENV.OS_INTERFACE")
OS_IDENTITY_API_VERSION string = config.GetString("openstack.ENV.OS_IDENTITY_API_VERSION")

// Paths for scripts to automate openstack user management
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, is there a reason why you went with executing a script versus using an Openstack GO SDK? I'm wondering if that might be a better approach when it comes to error handling and dependencies (now you would need to ensure the Openstack CLI is setup on the running machine)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also just noticed that the scripts are not checked into the repo. Thoughts on including those?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, is there a reason why you went with executing a script versus using an Openstack GO SDK?

Not gonna lie, I did not think about a GO package for interacting with Openstack API. I should have checked if there is one out of there. I will look into it and see if I can implement it.

I also just noticed that the scripts are not checked into the repo. Thoughts on including those?

Sure, no problem.

new_member string = config.GetString("openstack.SCRIPTS.new_member")
reset_password string = config.GetString("openstack.SCRIPTS.reset_password")
check_if_exists string = config.GetString("openstack.SCRIPTS.check_if_exists")
)

func Openstack() (*discordgo.ApplicationCommand, func(s *discordgo.Session, i *discordgo.InteractionCreate)) {
return &discordgo.ApplicationCommand{
Name: "openstack",
Description: "Create or reset your openstack account",
DefaultMemberPermissions: &permission.Member,
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "option",
Description: "Option of create or reset",
Required: true,
Choices: []*discordgo.ApplicationCommandOptionChoice{
{
Name: "Create",
Value: "Create",
},
{
Name: "Reset",
Value: "Reset",
},
},
},
},
},
func(s *discordgo.Session, i *discordgo.InteractionCreate) {
span := tracer.StartSpan(
"commands.slash.openstack:Openstack",
tracer.ResourceName("/openstack"),
)
defer span.Finish()

ssOption := i.ApplicationCommandData().Options[0].StringValue()
err := helpers.InitialMessage(s, i, fmt.Sprintf("You ran the /openstack command to %s your account!", strings.ToLower(ssOption)))
if err != nil {
logging.Error(s, err.Error(), i.Member.User, span)
}

// Initialize the environment variables for Openstack CLI
SetOpenstackRC()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since these variables are initially stored in the OBIII config file, I'd probably move this to the main.go file as it can be seen as redundant to keep setting these on every Openstack command. However, I do see the argument where the host's environment variables could get overwritten after OBIII's initial startup

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depending on gophercloud's implementation, I may or may not need it but in case if I do, I will move it to main.go.


err = helpers.UpdateMessage(s, i, "Checking your email...")
if err != nil {
logging.Error(s, err.Error(), i.Member.User, span)
}
// Get email and check if it is an actual email
email, err := data.User.GetEmail(i.Member.User.ID, span.Context())
if err != nil {
logging.Error(s, err.Error(), i.Member.User, span)
}
if email == "" {
logging.Debug(s, "User has no email", i.Member.User, span)
err = helpers.UpdateMessage(s, i, "You have no verified email. Run /member and verify your email and run this command again.")
if err != nil {
logging.Error(s, err.Error(), i.Member.User, span)
}
return
}

// Check if user exists on Openstack already
exists, err := CheckIfExists(email)
if err != nil {
logging.Error(s, err.Error(), i.Member.User, span)
return
}

if ssOption == "Create" {
// Check if user trying to create an account when it already has one
if exists {
logging.Debug(s, "User already has an openstack account and is trying to create one", i.Member.User, span)
err = helpers.UpdateMessage(s, i, "Openstack account already exisits. Run the reset option if you forgot your password.")
if err != nil {
logging.Error(s, err.Error(), i.Member.User, span)
}
return
}

// Checking if the user is DM'able
err = helpers.SendDirectMessage(s, i.Member.User.ID, "Checking to see if your DMs are open... your openstack account username and password will be sent here!", span.Context())
if err != nil {
logging.Debug(s, "User's DMs are not open", i.Member.User, span)
err = helpers.UpdateMessage(s, i, "Your DMs are not open! Please open your DMs and run the command again.")
if err != nil {
logging.Error(s, err.Error(), i.Member.User, span)
}
return
}

// Create the account
err = helpers.UpdateMessage(s, i, "Creating your account...")
if err != nil {
logging.Error(s, err.Error(), i.Member.User, span)
}
username, password, err := Create(email)
if err != nil {
logging.Error(s, err.Error(), i.Member.User, span)
return
}

// Send the username and password to the usuer via DM
message := fmt.Sprintf("Thank you for reaching out to us!\nHere are your credentials for RITSEC's Openstack:\n\nUsername: %s\nTemporary Password: %s\n\nPlease change the password\nOpenstack link: stack.ritsec.cloud", username, password)
logging.Debug(s, "Sent username and password to member", i.Member.User, span)
err = helpers.SendDirectMessage(s, i.Member.User.ID, message, span.Context())
if err != nil {
logging.Error(s, err.Error(), i.Member.User, span)
return
}

err = helpers.UpdateMessage(s, i, "Sent the username and password to your DMs, check your DMs!")
if err != nil {
logging.Error(s, err.Error(), i.Member.User, span)
}
} else if ssOption == "Reset" {
// Check if the user is trying to reset password on non-existent account
if !exists {
logging.Debug(s, "User does not have an openstack account and is trying to reset the password on it", i.Member.User, span)
err = helpers.UpdateMessage(s, i, "Openstack account does not exist and you are trying to reset it.")
if err != nil {
logging.Error(s, err.Error(), i.Member.User, span)
}
return
}

// Checking if the user is DM'able
err = helpers.SendDirectMessage(s, i.Member.User.ID, "Checking to see if your DMs are open... your openstack account username and password will be sent here!", span.Context())
if err != nil {
logging.Debug(s, "User's DMs are not open", i.Member.User, span)
err = helpers.UpdateMessage(s, i, "Your DMs are not open! Please open your DMs and run the command again.")
if err != nil {
logging.Error(s, err.Error(), i.Member.User, span)
}
return
}

// Reset the password of the account
err = helpers.UpdateMessage(s, i, "Resetting your account...")
if err != nil {
logging.Error(s, err.Error(), i.Member.User, span)
}
username, password, err := Reset(email)
logging.Debug(s, "User has the openstack account password reset", i.Member.User, span)
if err != nil {
logging.Error(s, err.Error(), i.Member.User, span)
return
}

message := fmt.Sprintf("Thank you for reaching out to us!\n Here are your credentials for RITSEC's Openstack:\n\nUsername: %s\nTemporary Password: %s\n\nPlease change the password\nOpenstack link: stack.ritsec.cloud", username, password)
err = helpers.SendDirectMessage(s, i.Member.User.ID, message, span.Context())
if err != nil {
logging.Error(s, err.Error(), i.Member.User, span)
return
}

err = helpers.UpdateMessage(s, i, "Sent the username and password to your DMs, check your DMs!")
if err != nil {
logging.Error(s, err.Error(), i.Member.User, span)
}
} else {
return
}
}
}

func Create(email string) (username string, password string, error error) {
createCmd := exec.Command(new_member, email)

stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
createCmd.Stdout = stdout
createCmd.Stderr = stderr

err := createCmd.Start()
if err != nil {
return "", "", err
}
err = createCmd.Wait()
if err != nil {
return "", "", err
}

output := strings.Fields(stdout.String())
username = output[0]
password = output[1]

return username, password, nil
}

func Reset(email string) (username string, password string, error error) {
resetCmd := exec.Command(reset_password, email)

stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
resetCmd.Stdout = stdout
resetCmd.Stderr = stderr

err := resetCmd.Start()
if err != nil {
return "", "", err
}
err = resetCmd.Wait()
if err != nil {
return "", "", err
}

output := strings.Fields(stdout.String())
username = output[0]
password = output[1]

return username, password, nil
}

func CheckIfExists(email string) (result bool, error error) {
checkIfExistsCmd := exec.Command(check_if_exists, email)

stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
checkIfExistsCmd.Stdout = stdout
checkIfExistsCmd.Stderr = stderr

err := checkIfExistsCmd.Run()
if err != nil {
return false, err
}

output := strings.TrimSpace(stdout.String())
if output == "0" {
return false, nil
} else {
return true, nil
}
}

func SetOpenstackRC() {
os.Setenv("OS_AUTH_URL", OS_AUTH_URL)
os.Setenv("OS_PROJECT_ID", OS_PROJECT_ID)
os.Setenv("OS_PROJECT_NAME", OS_PROJECT_NAME)
os.Setenv("OS_USER_DOMAIN_NAME", OS_USER_DOMAIN_NAME)
os.Setenv("OS_PROJECT_DOMAIN_ID", OS_PROJECT_DOMAIN_ID)
os.Setenv("OS_USERNAME", OS_USERNAME)
os.Setenv("OS_PASSWORD", OS_PASSWORD)
os.Setenv("OS_REGION_NAME", OS_REGION_NAME)
os.Setenv("OS_INTERFACE", OS_INTERFACE)
os.Setenv("OS_IDENTITY_API_VERSION", OS_IDENTITY_API_VERSION)
}
18 changes: 17 additions & 1 deletion config_example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,20 @@ commands:
channel_id:
vote:
url: http(s)://example.com(:port)

openstack:
ENV:
OS_AUTH_URL:
OS_PROJECT_ID:
OS_PROJECT_NAME:
OS_USER_DOMAIN_NAME:
OS_PROJECT_DOMAIN_ID:
OS_USERNAME:
OS_PASSWORD:
OS_REGION_NAME:
OS_INTERFACE:
OS_IDENTITY_API_VERSION:
# Absolute paths
SCRIPTS:
new_member:
reset_password:
check_if_exists:
26 changes: 21 additions & 5 deletions data/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func (*user_s) SetEmail(id string, email string, ctx ddtrace.SpanContext) (*ent.
Save(Ctx)
}

// SetEmail sets the email for a user
// IncrementVerificationAttempts increments the verification attempts after user tries to verify
func (*user_s) IncrementVerificationAttempts(id string, ctx ddtrace.SpanContext) (*ent.User, error) {
span := tracer.StartSpan(
"data.user:IncrementVerificationAttempts",
Expand All @@ -91,7 +91,7 @@ func (*user_s) IncrementVerificationAttempts(id string, ctx ddtrace.SpanContext)
Save(Ctx)
}

// SetEmail sets the email for a user
// MarkVerified marks the user verified
func (*user_s) MarkVerified(id string, ctx ddtrace.SpanContext) (*ent.User, error) {
span := tracer.StartSpan(
"data.user:MarkVerified",
Expand All @@ -110,7 +110,7 @@ func (*user_s) MarkVerified(id string, ctx ddtrace.SpanContext) (*ent.User, erro
Save(Ctx)
}

// SetEmail sets the email for a user
// IsVerified checks to see if the user is verified
func (*user_s) IsVerified(id string, ctx ddtrace.SpanContext) bool {
span := tracer.StartSpan(
"data.user:IsVerified",
Expand All @@ -127,7 +127,7 @@ func (*user_s) IsVerified(id string, ctx ddtrace.SpanContext) bool {
return entUser.Verified
}

// SetEmail sets the email for a user
// EmailExists checks to see if the email exists for the user
func (*user_s) EmailExists(id string, email string, ctx ddtrace.SpanContext) bool {
span := tracer.StartSpan(
"data.user:EmailExists",
Expand All @@ -151,7 +151,7 @@ func (*user_s) EmailExists(id string, email string, ctx ddtrace.SpanContext) boo
return exists
}

// SetEmail sets the email for a user
// GetVerificationAttempts returns the amount of verification attempts the user has
func (*user_s) GetVerificationAttempts(id string, ctx ddtrace.SpanContext) (int, error) {
span := tracer.StartSpan(
"data.user:GetVerificationAttempts",
Expand All @@ -167,3 +167,19 @@ func (*user_s) GetVerificationAttempts(id string, ctx ddtrace.SpanContext) (int,

return int(entUser.VerificationAttempts), nil
}

func (*user_s) GetEmail(id string, ctx ddtrace.SpanContext) (string, error) {
span := tracer.StartSpan(
"data.user:GetEmail",
tracer.ResourceName("Data.User.GetEmail"),
tracer.ChildOf(ctx),
)
defer span.Finish()

entUser, err := User.Get(id, span.Context())
if err != nil {
return "", err
}

return entUser.Email, nil
}
21 changes: 21 additions & 0 deletions helpers/message.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package helpers

import "github.com/bwmarrin/discordgo"

func InitialMessage(s *discordgo.Session, i *discordgo.InteractionCreate, message string) (error error) {
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Flags: discordgo.MessageFlagsEphemeral,
Content: message,
},
})
return err
}

func UpdateMessage(s *discordgo.Session, i *discordgo.InteractionCreate, message string) (error error) {
_, err := s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
Content: &message,
})
return err
}
Loading