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

Merged
merged 50 commits into from
Dec 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 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
dbd7447
Refactor into gophercloud (#151)
c0untingNumbers Nov 23, 2024
703196d
Added comments and used the enabled option from config
c0untingNumbers Nov 26, 2024
5e67259
lowered the string that Errorf was unhappy with
c0untingNumbers Nov 26, 2024
80a2ef3
ent go generate ./...
c0untingNumbers Nov 29, 2024
19cb144
new db for openstack reset timestamps
c0untingNumbers Nov 29, 2024
7866f39
data queries for openstack db
c0untingNumbers Nov 29, 2024
9bc1aae
Added a prevention for reset spamming
c0untingNumbers Nov 29, 2024
0334c26
go generate ./...
c0untingNumbers Nov 29, 2024
992f99e
Added missing check for error
c0untingNumbers Nov 29, 2024
0eada9f
Clarified the instructions
c0untingNumbers Nov 29, 2024
eb8dc3e
Added and enabled the new command verify
c0untingNumbers Nov 29, 2024
5da7a84
Added missing error checks
c0untingNumbers Nov 29, 2024
686d44d
added comments
c0untingNumbers Dec 6, 2024
7a8786f
restructured to use structs for easy modifying defaults
c0untingNumbers Dec 6, 2024
903f07a
Removed helper messages
c0untingNumbers Dec 6, 2024
f52d8da
Updated the message content to reflect the stupid RIT's filtiering em…
c0untingNumbers Dec 10, 2024
a1b1a18
Better error checking to make sure the timestamp exists
c0untingNumbers Dec 10, 2024
5a9b2f5
added function to check if user has done /member first
c0untingNumbers Dec 18, 2024
86a25f4
removed isverified
c0untingNumbers Dec 18, 2024
b50e51c
Editted the response to show the right command
c0untingNumbers Dec 18, 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
8 changes: 5 additions & 3 deletions .github/workflows/checks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: '1.20'
go-version: '1.23.0'
cache: false
- name: download dependencies
run: go mod tidy
- name: Compile
run: go build main.go
- name: Unit Tests
run: go test -v ./...
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
uses: golangci/golangci-lint-action@v6
with:
version: v1.54
version: v1.62
args: --timeout=10m
2 changes: 2 additions & 0 deletions commands/enabled.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ func populateSlashCommands(ctx ddtrace.SpanContext) {
SlashCommands["dquery"] = slash.DQuery
SlashCommands["attendance"] = slash.Attendance
SlashCommands["attendanceof"] = slash.Attendanceof
SlashCommands["openstack"] = slash.Openstack
SlashCommands["verify"] = slash.Verify
}

// populateHandlers populates the Handlers map with all of the handlers
Expand Down
2 changes: 1 addition & 1 deletion commands/slash/member.go
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,7 @@ func recievedEmail(s *discordgo.Session, i *discordgo.InteractionCreate, userEma
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseUpdateMessage,
Data: &discordgo.InteractionResponseData{
Content: "A verification code has been sent to \"" + userEmail + "\". This could take up to 10 minutes and could be in your spam. Please check your spam! **Do not close discord or this window will be closed**.",
Content: "A verification code has been sent to \"" + userEmail + "\". This email will take at least **12 minutes** to arrive and show up in the **spam** due to RIT's email filitering system. Please be patient and watch your spam box! **Do not close discord or this window will be closed**.",
Components: []discordgo.MessageComponent{
discordgo.ActionsRow{
Components: []discordgo.MessageComponent{
Expand Down
221 changes: 221 additions & 0 deletions commands/slash/openstack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
package slash

import (
"fmt"
"strings"
"time"

"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"
"github.com/ritsec/ops-bot-iii/osclient"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
)

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) {
if config.Openstack.Enabled {
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)
}

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 /verify to add 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 := osclient.CheckUserExists(email)
if err != nil {
logging.Error(s, err.Error(), i.Member.User, span)
return
}

if ssOption == "Create" {
// Stop here 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 := osclient.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
}

// Check to see if the user's reset timestamp exists
ts_exists, err := data.Openstack.Exists(i.Member.User.ID, span.Context())
if err != nil {
logging.Error(s, err.Error(), i.Member.User, span)
return
}
if !ts_exists {
// Create the row for user's timestamp and don't check it
_, err = data.Openstack.Create(i.Member.User.ID, span.Context())
if err != nil {
logging.Error(s, err.Error(), i.Member.User, span)
return
}
} else {
// Check the user's timestamp if has done recently

// Get the user's openstacks info
openstack_ent, err := data.Openstack.Get(i.Member.User.ID, span.Context())
if err != nil {
logging.Error(s, err.Error(), i.Member.User, span)
return
}
tx, err := time.LoadLocation("America/New_York")
if err != nil {
logging.Error(s, err.Error(), i.Member.User, span)
return
}
// Check if the user has done the reset recently
if time.Now().In(tx).Sub(openstack_ent.Timestamp) >= -2*time.Hour && time.Now().In(tx).Sub(openstack_ent.Timestamp) <= 2*time.Hour {
err = helpers.UpdateMessage(s, i, "You have tried to reset too much in the past 2 hours, please wait before trying again!")
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 := osclient.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)
}

// Update the timestamp for the last reset for the user
_, err = data.Openstack.Update(i.Member.User.ID, span.Context())
if err != nil {
logging.Error(s, err.Error(), i.Member.User, span)
return
}
} else {
return
}
}
}
}
138 changes: 138 additions & 0 deletions commands/slash/verify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package slash

import (
"fmt"
"strings"

"github.com/bwmarrin/discordgo"
"github.com/ritsec/ops-bot-iii/data"
"github.com/ritsec/ops-bot-iii/logging"
"github.com/ritsec/ops-bot-iii/mail"
"github.com/sirupsen/logrus"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
)

// The purpose of this command is for users who were manually verified through the /member command.
// The issue is that they then would have no email in the database but they still need to set their email
// if they want to use the openstack command to create their accounts.
//
// This is done by checking if their verification attempts is above 1, arbitrary locking out this command until they did /member command first.
func Verify() (*discordgo.ApplicationCommand, func(s *discordgo.Session, i *discordgo.InteractionCreate)) {
return &discordgo.ApplicationCommand{
Name: "verify",
Description: "Verify your email to use services like openstack and count for attendance",
},
func(s *discordgo.Session, i *discordgo.InteractionCreate) {
span := tracer.StartSpan(
"commands.slash.verify:Verify",
tracer.ResourceName("/Verify"),
)
defer span.Finish()

logging.Debug(s, "Verify command received", i.Member.User, span)

attempts, err := data.User.GetVerificationAttempts(i.Member.User.ID, span.Context())
if err != nil {
logging.Error(s, err.Error(), i.Member.User, span, logrus.Fields{"error": err})
return
}

if attempts == 0 {
logging.Debug(s, "User has not used /member yet", i.Member.User, span)
return
}

// check if user has an RIT userEmail
ritEmail, i, err := hasRITEmail(s, i, span.Context())
if err != nil {
logging.Error(s, err.Error(), i.Member.User, span, logrus.Fields{"error": err})
return
}

if ritEmail {
// get userEmail
userEmail, i, err := getEmail(s, i, span.Context())
if err != nil {
logging.Error(s, err.Error(), i.Member.User, span, logrus.Fields{"error": err})
return
}

logging.Debug(s, fmt.Sprintf("User provided email: `%v`", userEmail), i.Member.User, span)

// check if userEmail is valid
if !validRITEmail(userEmail, span.Context()) {
logging.Debug(s, fmt.Sprintf("User has invalid RIT email: `%v`", userEmail), i.Member.User, span)
return
}

// check if email is already in use
if data.User.EmailExists(i.Member.User.ID, userEmail, span.Context()) {
logging.Debug(s, fmt.Sprintf("User has already used email: `%v`", userEmail), i.Member.User, span)
return
}

// send userEmail
code, err := mail.SendVerificationEmail(userEmail, span.Context())
if err != nil {
logging.Error(s, err.Error(), i.Member.User, span, logrus.Fields{"error": err})
return
}

logging.Debug(s, fmt.Sprintf("User send Email with verification code: `%v`", code), i.Member.User, span)

// check if userEmail was recieved
recieved, i, err := recievedEmail(s, i, userEmail, span.Context())
if err != nil {
logging.Error(s, err.Error(), i.Member.User, span, logrus.Fields{"error": err})
return
}

if recieved {

logging.Debug(s, fmt.Sprintf("User recieved email: `%v`", userEmail), i.Member.User, span)

// get verification code
verificationCode, i, err := getVerificationCode(s, i, span.Context())
if err != nil {
logging.Error(s, err.Error(), i.Member.User, span, logrus.Fields{"error": err})
return
}

// check code
if strings.TrimSpace(code) != strings.TrimSpace(verificationCode) {
logging.Debug(s, "User provided invalid verification code", i.Member.User, span)
err := invalidCode(s, i, verificationCode, 0, span.Context())
if err != nil {
logging.Error(s, err.Error(), i.Member.User, span, logrus.Fields{"error": err})
}
return
}

// add email to user
_, err = data.User.SetEmail(i.Member.User.ID, userEmail, span.Context())
if err != nil {
logging.Error(s, err.Error(), i.Member.User, span, logrus.Fields{"error": err})
return
}

// mark user as verified
_, err = data.User.MarkVerified(i.Member.User.ID, span.Context())
if err != nil {
logging.Error(s, err.Error(), i.Member.User, span, logrus.Fields{"error": err})
return
}

msg := "You have been verified as a member of RITSEC. Welcome!"
_, err = s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
Content: &msg,
})
if err != nil {
logging.Error(s, err.Error(), i.User, span, logrus.Fields{"error": err})
return
}
} else {
return
}
}
}
}
Loading
Loading