Compare commits
1 Commits
develop
...
gek/user-r
Author | SHA1 | Date |
---|---|---|
|
cff76707f0 | 2 years ago |
17 changed files with 878 additions and 858 deletions
@ -1,28 +0,0 @@
@@ -1,28 +0,0 @@
|
||||
{ |
||||
"cSpell.words": [ |
||||
"Debugln", |
||||
"Errorln", |
||||
"Fediverse", |
||||
"Ffmpeg", |
||||
"ffmpegpath", |
||||
"ffmpg", |
||||
"geoip", |
||||
"gosec", |
||||
"mattn", |
||||
"Mbps", |
||||
"nolint", |
||||
"Owncast", |
||||
"ppid", |
||||
"preact", |
||||
"RTMP", |
||||
"rtmpserverport", |
||||
"sqlite", |
||||
"Tracef", |
||||
"Traceln", |
||||
"upgrader", |
||||
"Upgrader", |
||||
"videojs", |
||||
"Warnf", |
||||
"Warnln" |
||||
] |
||||
} |
@ -1,9 +1,9 @@
@@ -1,9 +1,9 @@
|
||||
package events |
||||
|
||||
import "github.com/owncast/owncast/core/user" |
||||
import "github.com/owncast/owncast/models" |
||||
|
||||
// ConnectedClientInfo represents the information about a connected client.
|
||||
type ConnectedClientInfo struct { |
||||
Event |
||||
User *user.User `json:"user"` |
||||
User *models.User `json:"user"` |
||||
} |
||||
|
@ -1,311 +0,0 @@
@@ -1,311 +0,0 @@
|
||||
package user |
||||
|
||||
import ( |
||||
"context" |
||||
"database/sql" |
||||
"strings" |
||||
"time" |
||||
|
||||
"github.com/owncast/owncast/utils" |
||||
"github.com/pkg/errors" |
||||
log "github.com/sirupsen/logrus" |
||||
"github.com/teris-io/shortid" |
||||
) |
||||
|
||||
// ExternalAPIUser represents a single 3rd party integration that uses an access token.
|
||||
// This struct mostly matches the User struct so they can be used interchangeably.
|
||||
type ExternalAPIUser struct { |
||||
CreatedAt time.Time `json:"createdAt"` |
||||
LastUsedAt *time.Time `json:"lastUsedAt,omitempty"` |
||||
ID string `json:"id"` |
||||
AccessToken string `json:"accessToken"` |
||||
DisplayName string `json:"displayName"` |
||||
Type string `json:"type,omitempty"` // Should be API
|
||||
Scopes []string `json:"scopes"` |
||||
DisplayColor int `json:"displayColor"` |
||||
IsBot bool `json:"isBot"` |
||||
} |
||||
|
||||
const ( |
||||
// ScopeCanSendChatMessages will allow sending chat messages as itself.
|
||||
ScopeCanSendChatMessages = "CAN_SEND_MESSAGES" |
||||
// ScopeCanSendSystemMessages will allow sending chat messages as the system.
|
||||
ScopeCanSendSystemMessages = "CAN_SEND_SYSTEM_MESSAGES" |
||||
// ScopeHasAdminAccess will allow performing administrative actions on the server.
|
||||
ScopeHasAdminAccess = "HAS_ADMIN_ACCESS" |
||||
) |
||||
|
||||
// For a scope to be seen as "valid" it must live in this slice.
|
||||
var validAccessTokenScopes = []string{ |
||||
ScopeCanSendChatMessages, |
||||
ScopeCanSendSystemMessages, |
||||
ScopeHasAdminAccess, |
||||
} |
||||
|
||||
// InsertExternalAPIUser will add a new API user to the database.
|
||||
func InsertExternalAPIUser(token string, name string, color int, scopes []string) error { |
||||
log.Traceln("Adding new API user") |
||||
|
||||
_datastore.DbLock.Lock() |
||||
defer _datastore.DbLock.Unlock() |
||||
|
||||
scopesString := strings.Join(scopes, ",") |
||||
id := shortid.MustGenerate() |
||||
|
||||
tx, err := _datastore.DB.Begin() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
stmt, err := tx.Prepare("INSERT INTO users(id, display_name, display_color, scopes, type, previous_names) values(?, ?, ?, ?, ?, ?)") |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer stmt.Close() |
||||
|
||||
if _, err = stmt.Exec(id, name, color, scopesString, "API", name); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if err = tx.Commit(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if err := addAccessTokenForUser(token, id); err != nil { |
||||
return errors.Wrap(err, "unable to save access token for new external api user") |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// DeleteExternalAPIUser will delete a token from the database.
|
||||
func DeleteExternalAPIUser(token string) error { |
||||
log.Traceln("Deleting access token") |
||||
|
||||
_datastore.DbLock.Lock() |
||||
defer _datastore.DbLock.Unlock() |
||||
|
||||
tx, err := _datastore.DB.Begin() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
stmt, err := tx.Prepare("UPDATE users SET disabled_at = CURRENT_TIMESTAMP WHERE id = (SELECT user_id FROM user_access_tokens WHERE token = ?)") |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer stmt.Close() |
||||
|
||||
result, err := stmt.Exec(token) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if rowsDeleted, _ := result.RowsAffected(); rowsDeleted == 0 { |
||||
tx.Rollback() //nolint
|
||||
return errors.New(token + " not found") |
||||
} |
||||
|
||||
if err = tx.Commit(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// GetExternalAPIUserForAccessTokenAndScope will determine if a specific token has access to perform a scoped action.
|
||||
func GetExternalAPIUserForAccessTokenAndScope(token string, scope string) (*ExternalAPIUser, error) { |
||||
// This will split the scopes from comma separated to individual rows
|
||||
// so we can efficiently find if a token supports a single scope.
|
||||
// This is SQLite specific, so if we ever support other database
|
||||
// backends we need to support other methods.
|
||||
query := `SELECT |
||||
id, |
||||
scopes, |
||||
display_name, |
||||
display_color, |
||||
created_at, |
||||
last_used |
||||
FROM |
||||
user_access_tokens |
||||
INNER JOIN ( |
||||
WITH RECURSIVE split( |
||||
id, |
||||
scopes, |
||||
display_name, |
||||
display_color, |
||||
created_at, |
||||
last_used, |
||||
disabled_at, |
||||
scope, |
||||
rest |
||||
) AS ( |
||||
SELECT |
||||
id, |
||||
scopes, |
||||
display_name, |
||||
display_color, |
||||
created_at, |
||||
last_used, |
||||
disabled_at, |
||||
'', |
||||
scopes || ',' |
||||
FROM |
||||
users AS u |
||||
UNION ALL |
||||
SELECT |
||||
id, |
||||
scopes, |
||||
display_name, |
||||
display_color, |
||||
created_at, |
||||
last_used, |
||||
disabled_at, |
||||
substr(rest, 0, instr(rest, ',')), |
||||
substr(rest, instr(rest, ',') + 1) |
||||
FROM |
||||
split |
||||
WHERE |
||||
rest <> '' |
||||
) |
||||
SELECT |
||||
id, |
||||
display_name, |
||||
display_color, |
||||
created_at, |
||||
last_used, |
||||
disabled_at, |
||||
scopes, |
||||
scope |
||||
FROM |
||||
split |
||||
WHERE |
||||
scope <> '' |
||||
) ON user_access_tokens.user_id = id |
||||
WHERE |
||||
disabled_at IS NULL |
||||
AND token = ? |
||||
AND scope = ?;` |
||||
|
||||
row := _datastore.DB.QueryRow(query, token, scope) |
||||
integration, err := makeExternalAPIUserFromRow(row) |
||||
|
||||
return integration, err |
||||
} |
||||
|
||||
// GetIntegrationNameForAccessToken will return the integration name associated with a specific access token.
|
||||
func GetIntegrationNameForAccessToken(token string) *string { |
||||
name, err := _datastore.GetQueries().GetUserDisplayNameByToken(context.Background(), token) |
||||
if err != nil { |
||||
return nil |
||||
} |
||||
|
||||
return &name |
||||
} |
||||
|
||||
// GetExternalAPIUser will return all API users with access tokens.
|
||||
func GetExternalAPIUser() ([]ExternalAPIUser, error) { //nolint
|
||||
query := "SELECT id, token, display_name, display_color, scopes, created_at, last_used FROM users, user_access_tokens WHERE user_access_tokens.user_id = id AND type IS 'API' AND disabled_at IS NULL" |
||||
|
||||
rows, err := _datastore.DB.Query(query) |
||||
if err != nil { |
||||
return []ExternalAPIUser{}, err |
||||
} |
||||
defer rows.Close() |
||||
|
||||
integrations, err := makeExternalAPIUsersFromRows(rows) |
||||
|
||||
return integrations, err |
||||
} |
||||
|
||||
// SetExternalAPIUserAccessTokenAsUsed will update the last used timestamp for a token.
|
||||
func SetExternalAPIUserAccessTokenAsUsed(token string) error { |
||||
tx, err := _datastore.DB.Begin() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
stmt, err := tx.Prepare("UPDATE users SET last_used = CURRENT_TIMESTAMP WHERE id = (SELECT user_id FROM user_access_tokens WHERE token = ?)") |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer stmt.Close() |
||||
|
||||
if _, err := stmt.Exec(token); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if err = tx.Commit(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func makeExternalAPIUserFromRow(row *sql.Row) (*ExternalAPIUser, error) { |
||||
var id string |
||||
var displayName string |
||||
var displayColor int |
||||
var scopes string |
||||
var createdAt time.Time |
||||
var lastUsedAt *time.Time |
||||
|
||||
err := row.Scan(&id, &scopes, &displayName, &displayColor, &createdAt, &lastUsedAt) |
||||
if err != nil { |
||||
log.Debugln("unable to convert row to api user", err) |
||||
return nil, err |
||||
} |
||||
|
||||
integration := ExternalAPIUser{ |
||||
ID: id, |
||||
DisplayName: displayName, |
||||
DisplayColor: displayColor, |
||||
CreatedAt: createdAt, |
||||
Scopes: strings.Split(scopes, ","), |
||||
LastUsedAt: lastUsedAt, |
||||
} |
||||
|
||||
return &integration, nil |
||||
} |
||||
|
||||
func makeExternalAPIUsersFromRows(rows *sql.Rows) ([]ExternalAPIUser, error) { |
||||
integrations := make([]ExternalAPIUser, 0) |
||||
|
||||
for rows.Next() { |
||||
var id string |
||||
var accessToken string |
||||
var displayName string |
||||
var displayColor int |
||||
var scopes string |
||||
var createdAt time.Time |
||||
var lastUsedAt *time.Time |
||||
|
||||
err := rows.Scan(&id, &accessToken, &displayName, &displayColor, &scopes, &createdAt, &lastUsedAt) |
||||
if err != nil { |
||||
log.Errorln(err) |
||||
return nil, err |
||||
} |
||||
|
||||
integration := ExternalAPIUser{ |
||||
ID: id, |
||||
AccessToken: accessToken, |
||||
DisplayName: displayName, |
||||
DisplayColor: displayColor, |
||||
CreatedAt: createdAt, |
||||
Scopes: strings.Split(scopes, ","), |
||||
LastUsedAt: lastUsedAt, |
||||
IsBot: true, |
||||
} |
||||
integrations = append(integrations, integration) |
||||
} |
||||
|
||||
return integrations, nil |
||||
} |
||||
|
||||
// HasValidScopes will verify that all the scopes provided are valid.
|
||||
func HasValidScopes(scopes []string) bool { |
||||
for _, scope := range scopes { |
||||
_, foundInSlice := utils.FindInSlice(validAccessTokenScopes, scope) |
||||
if !foundInSlice { |
||||
return false |
||||
} |
||||
} |
||||
return true |
||||
} |
@ -1,473 +0,0 @@
@@ -1,473 +0,0 @@
|
||||
package user |
||||
|
||||
import ( |
||||
"context" |
||||
"database/sql" |
||||
"fmt" |
||||
"sort" |
||||
"strings" |
||||
"time" |
||||
|
||||
"github.com/owncast/owncast/config" |
||||
"github.com/owncast/owncast/core/data" |
||||
"github.com/owncast/owncast/db" |
||||
"github.com/owncast/owncast/utils" |
||||
"github.com/pkg/errors" |
||||
"github.com/teris-io/shortid" |
||||
|
||||
log "github.com/sirupsen/logrus" |
||||
) |
||||
|
||||
var _datastore *data.Datastore |
||||
|
||||
const ( |
||||
moderatorScopeKey = "MODERATOR" |
||||
minSuggestedUsernamePoolLength = 10 |
||||
) |
||||
|
||||
// User represents a single chat user.
|
||||
type User struct { |
||||
CreatedAt time.Time `json:"createdAt"` |
||||
DisabledAt *time.Time `json:"disabledAt,omitempty"` |
||||
NameChangedAt *time.Time `json:"nameChangedAt,omitempty"` |
||||
AuthenticatedAt *time.Time `json:"-"` |
||||
ID string `json:"id"` |
||||
DisplayName string `json:"displayName"` |
||||
PreviousNames []string `json:"previousNames"` |
||||
Scopes []string `json:"scopes,omitempty"` |
||||
DisplayColor int `json:"displayColor"` |
||||
IsBot bool `json:"isBot"` |
||||
Authenticated bool `json:"authenticated"` |
||||
} |
||||
|
||||
// IsEnabled will return if this single user is enabled.
|
||||
func (u *User) IsEnabled() bool { |
||||
return u.DisabledAt == nil |
||||
} |
||||
|
||||
// IsModerator will return if the user has moderation privileges.
|
||||
func (u *User) IsModerator() bool { |
||||
_, hasModerationScope := utils.FindInSlice(u.Scopes, moderatorScopeKey) |
||||
return hasModerationScope |
||||
} |
||||
|
||||
// SetupUsers will perform the initial initialization of the user package.
|
||||
func SetupUsers() { |
||||
_datastore = data.GetDatastore() |
||||
} |
||||
|
||||
func generateDisplayName() string { |
||||
suggestedUsernamesList := data.GetSuggestedUsernamesList() |
||||
|
||||
if len(suggestedUsernamesList) >= minSuggestedUsernamePoolLength { |
||||
index := utils.RandomIndex(len(suggestedUsernamesList)) |
||||
return suggestedUsernamesList[index] |
||||
} else { |
||||
return utils.GeneratePhrase() |
||||
} |
||||
} |
||||
|
||||
// CreateAnonymousUser will create a new anonymous user with the provided display name.
|
||||
func CreateAnonymousUser(displayName string) (*User, string, error) { |
||||
// Try to assign a name that was requested.
|
||||
if displayName != "" { |
||||
// If name isn't available then generate a random one.
|
||||
if available, _ := IsDisplayNameAvailable(displayName); !available { |
||||
displayName = generateDisplayName() |
||||
} |
||||
} else { |
||||
displayName = generateDisplayName() |
||||
} |
||||
|
||||
displayColor := utils.GenerateRandomDisplayColor(config.MaxUserColor) |
||||
|
||||
id := shortid.MustGenerate() |
||||
user := &User{ |
||||
ID: id, |
||||
DisplayName: displayName, |
||||
DisplayColor: displayColor, |
||||
CreatedAt: time.Now(), |
||||
} |
||||
|
||||
// Create new user.
|
||||
if err := create(user); err != nil { |
||||
return nil, "", err |
||||
} |
||||
|
||||
// Assign it an access token.
|
||||
accessToken, err := utils.GenerateAccessToken() |
||||
if err != nil { |
||||
log.Errorln("Unable to create access token for new user") |
||||
return nil, "", err |
||||
} |
||||
if err := addAccessTokenForUser(accessToken, id); err != nil { |
||||
return nil, "", errors.Wrap(err, "unable to save access token for new user") |
||||
} |
||||
|
||||
return user, accessToken, nil |
||||
} |
||||
|
||||
// IsDisplayNameAvailable will check if the proposed name is available for use.
|
||||
func IsDisplayNameAvailable(displayName string) (bool, error) { |
||||
if available, err := _datastore.GetQueries().IsDisplayNameAvailable(context.Background(), displayName); err != nil { |
||||
return false, errors.Wrap(err, "unable to check if display name is available") |
||||
} else if available != 0 { |
||||
return false, nil |
||||
} |
||||
|
||||
return true, nil |
||||
} |
||||
|
||||
// ChangeUsername will change the user associated to userID from one display name to another.
|
||||
func ChangeUsername(userID string, username string) error { |
||||
_datastore.DbLock.Lock() |
||||
defer _datastore.DbLock.Unlock() |
||||
|
||||
if err := _datastore.GetQueries().ChangeDisplayName(context.Background(), db.ChangeDisplayNameParams{ |
||||
DisplayName: username, |
||||
ID: userID, |
||||
PreviousNames: sql.NullString{String: fmt.Sprintf(",%s", username), Valid: true}, |
||||
NamechangedAt: sql.NullTime{Time: time.Now(), Valid: true}, |
||||
}); err != nil { |
||||
return errors.Wrap(err, "unable to change display name") |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// ChangeUserColor will change the user associated to userID from one display name to another.
|
||||
func ChangeUserColor(userID string, color int) error { |
||||
_datastore.DbLock.Lock() |
||||
defer _datastore.DbLock.Unlock() |
||||
|
||||
if err := _datastore.GetQueries().ChangeDisplayColor(context.Background(), db.ChangeDisplayColorParams{ |
||||
DisplayColor: int32(color), |
||||
ID: userID, |
||||
}); err != nil { |
||||
return errors.Wrap(err, "unable to change display color") |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func addAccessTokenForUser(accessToken, userID string) error { |
||||
return _datastore.GetQueries().AddAccessTokenForUser(context.Background(), db.AddAccessTokenForUserParams{ |
||||
Token: accessToken, |
||||
UserID: userID, |
||||
}) |
||||
} |
||||
|
||||
func create(user *User) error { |
||||
_datastore.DbLock.Lock() |
||||
defer _datastore.DbLock.Unlock() |
||||
|
||||
tx, err := _datastore.DB.Begin() |
||||
if err != nil { |
||||
log.Debugln(err) |
||||
} |
||||
defer func() { |
||||
_ = tx.Rollback() |
||||
}() |
||||
|
||||
stmt, err := tx.Prepare("INSERT INTO users(id, display_name, display_color, previous_names, created_at) values(?, ?, ?, ?, ?)") |
||||
if err != nil { |
||||
log.Debugln(err) |
||||
} |
||||
defer stmt.Close() |
||||
|
||||
_, err = stmt.Exec(user.ID, user.DisplayName, user.DisplayColor, user.DisplayName, user.CreatedAt) |
||||
if err != nil { |
||||
log.Errorln("error creating new user", err) |
||||
return err |
||||
} |
||||
|
||||
return tx.Commit() |
||||
} |
||||
|
||||
// SetEnabled will set the enabled status of a single user by ID.
|
||||
func SetEnabled(userID string, enabled bool) error { |
||||
_datastore.DbLock.Lock() |
||||
defer _datastore.DbLock.Unlock() |
||||
|
||||
tx, err := _datastore.DB.Begin() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
defer tx.Rollback() //nolint
|
||||
|
||||
var stmt *sql.Stmt |
||||
if !enabled { |
||||
stmt, err = tx.Prepare("UPDATE users SET disabled_at=DATETIME('now', 'localtime') WHERE id IS ?") |
||||
} else { |
||||
stmt, err = tx.Prepare("UPDATE users SET disabled_at=null WHERE id IS ?") |
||||
} |
||||
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
defer stmt.Close() |
||||
|
||||
if _, err := stmt.Exec(userID); err != nil { |
||||
return err |
||||
} |
||||
|
||||
return tx.Commit() |
||||
} |
||||
|
||||
// GetUserByToken will return a user by an access token.
|
||||
func GetUserByToken(token string) *User { |
||||
u, err := _datastore.GetQueries().GetUserByAccessToken(context.Background(), token) |
||||
if err != nil { |
||||
return nil |
||||
} |
||||
|
||||
var scopes []string |
||||
if u.Scopes.Valid { |
||||
scopes = strings.Split(u.Scopes.String, ",") |
||||
} |
||||
|
||||
var disabledAt *time.Time |
||||
if u.DisabledAt.Valid { |
||||
disabledAt = &u.DisabledAt.Time |
||||
} |
||||
|
||||
var authenticatedAt *time.Time |
||||
if u.AuthenticatedAt.Valid { |
||||
authenticatedAt = &u.AuthenticatedAt.Time |
||||
} |
||||
|
||||
return &User{ |
||||
ID: u.ID, |
||||
DisplayName: u.DisplayName, |
||||
DisplayColor: int(u.DisplayColor), |
||||
CreatedAt: u.CreatedAt.Time, |
||||
DisabledAt: disabledAt, |
||||
PreviousNames: strings.Split(u.PreviousNames.String, ","), |
||||
NameChangedAt: &u.NamechangedAt.Time, |
||||
AuthenticatedAt: authenticatedAt, |
||||
Authenticated: authenticatedAt != nil, |
||||
Scopes: scopes, |
||||
} |
||||
} |
||||
|
||||
// SetAccessTokenToOwner will reassign an access token to be owned by a
|
||||
// different user. Used for logging in with external auth.
|
||||
func SetAccessTokenToOwner(token, userID string) error { |
||||
return _datastore.GetQueries().SetAccessTokenToOwner(context.Background(), db.SetAccessTokenToOwnerParams{ |
||||
UserID: userID, |
||||
Token: token, |
||||
}) |
||||
} |
||||
|
||||
// SetUserAsAuthenticated will mark that a user has been authenticated
|
||||
// in some way.
|
||||
func SetUserAsAuthenticated(userID string) error { |
||||
return errors.Wrap(_datastore.GetQueries().SetUserAsAuthenticated(context.Background(), userID), "unable to set user as authenticated") |
||||
} |
||||
|
||||
// SetModerator will add or remove moderator status for a single user by ID.
|
||||
func SetModerator(userID string, isModerator bool) error { |
||||
if isModerator { |
||||
return addScopeToUser(userID, moderatorScopeKey) |
||||
} |
||||
|
||||
return removeScopeFromUser(userID, moderatorScopeKey) |
||||
} |
||||
|
||||
func addScopeToUser(userID string, scope string) error { |
||||
u := GetUserByID(userID) |
||||
if u == nil { |
||||
return errors.New("user not found when modifying scope") |
||||
} |
||||
|
||||
scopesString := u.Scopes |
||||
scopes := utils.StringSliceToMap(scopesString) |
||||
scopes[scope] = true |
||||
|
||||
scopesSlice := utils.StringMapKeys(scopes) |
||||
|
||||
return setScopesOnUser(userID, scopesSlice) |
||||
} |
||||
|
||||
func removeScopeFromUser(userID string, scope string) error { |
||||
u := GetUserByID(userID) |
||||
scopesString := u.Scopes |
||||
scopes := utils.StringSliceToMap(scopesString) |
||||
delete(scopes, scope) |
||||
|
||||
scopesSlice := utils.StringMapKeys(scopes) |
||||
|
||||
return setScopesOnUser(userID, scopesSlice) |
||||
} |
||||
|
||||
func setScopesOnUser(userID string, scopes []string) error { |
||||
_datastore.DbLock.Lock() |
||||
defer _datastore.DbLock.Unlock() |
||||
|
||||
tx, err := _datastore.DB.Begin() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
defer tx.Rollback() //nolint
|
||||
|
||||
scopesSliceString := strings.TrimSpace(strings.Join(scopes, ",")) |
||||
stmt, err := tx.Prepare("UPDATE users SET scopes=? WHERE id IS ?") |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
defer stmt.Close() |
||||
|
||||
var val *string |
||||
if scopesSliceString == "" { |
||||
val = nil |
||||
} else { |
||||
val = &scopesSliceString |
||||
} |
||||
|
||||
if _, err := stmt.Exec(val, userID); err != nil { |
||||
return err |
||||
} |
||||
|
||||
return tx.Commit() |
||||
} |
||||
|
||||
// GetUserByID will return a user by a user ID.
|
||||
func GetUserByID(id string) *User { |
||||
_datastore.DbLock.Lock() |
||||
defer _datastore.DbLock.Unlock() |
||||
|
||||
query := "SELECT id, display_name, display_color, created_at, disabled_at, previous_names, namechanged_at, scopes FROM users WHERE id = ?" |
||||
row := _datastore.DB.QueryRow(query, id) |
||||
if row == nil { |
||||
log.Errorln(row) |
||||
return nil |
||||
} |
||||
return getUserFromRow(row) |
||||
} |
||||
|
||||
// GetDisabledUsers will return back all the currently disabled users that are not API users.
|
||||
func GetDisabledUsers() []*User { |
||||
query := "SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at FROM users WHERE disabled_at IS NOT NULL AND type IS NOT 'API'" |
||||
|
||||
rows, err := _datastore.DB.Query(query) |
||||
if err != nil { |
||||
log.Errorln(err) |
||||
return nil |
||||
} |
||||
defer rows.Close() |
||||
|
||||
users := getUsersFromRows(rows) |
||||
|
||||
sort.Slice(users, func(i, j int) bool { |
||||
return users[i].DisabledAt.Before(*users[j].DisabledAt) |
||||
}) |
||||
|
||||
return users |
||||
} |
||||
|
||||
// GetModeratorUsers will return a list of users with moderator access.
|
||||
func GetModeratorUsers() []*User { |
||||
query := `SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at FROM ( |
||||
WITH RECURSIVE split(id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at, scope, rest) AS ( |
||||
SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at, '', scopes || ',' FROM users |
||||
UNION ALL |
||||
SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at, |
||||
substr(rest, 0, instr(rest, ',')), |
||||
substr(rest, instr(rest, ',')+1) |
||||
FROM split |
||||
WHERE rest <> '') |
||||
SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at, scope |
||||
FROM split |
||||
WHERE scope <> '' |
||||
ORDER BY created_at |
||||
) AS token WHERE token.scope = ?` |
||||
|
||||
rows, err := _datastore.DB.Query(query, moderatorScopeKey) |
||||
if err != nil { |
||||
log.Errorln(err) |
||||
return nil |
||||
} |
||||
defer rows.Close() |
||||
|
||||
users := getUsersFromRows(rows) |
||||
|
||||
return users |
||||
} |
||||
|
||||
func getUsersFromRows(rows *sql.Rows) []*User { |
||||
users := make([]*User, 0) |
||||
|
||||
for rows.Next() { |
||||
var id string |
||||
var displayName string |
||||
var displayColor int |
||||
var createdAt time.Time |
||||
var disabledAt *time.Time |
||||
var previousUsernames string |
||||
var userNameChangedAt *time.Time |
||||
var scopesString *string |
||||
|
||||
if err := rows.Scan(&id, &displayName, &scopesString, &displayColor, &createdAt, &disabledAt, &previousUsernames, &userNameChangedAt); err != nil { |
||||
log.Errorln("error creating collection of users from results", err) |
||||
return nil |
||||
} |
||||
|
||||
var scopes []string |
||||
if scopesString != nil { |
||||
scopes = strings.Split(*scopesString, ",") |
||||
} |
||||
|
||||
user := &User{ |
||||
ID: id, |
||||
DisplayName: displayName, |
||||
DisplayColor: displayColor, |
||||
CreatedAt: createdAt, |
||||
DisabledAt: disabledAt, |
||||
PreviousNames: strings.Split(previousUsernames, ","), |
||||
NameChangedAt: userNameChangedAt, |
||||
Scopes: scopes, |
||||
} |
||||
users = append(users, user) |
||||
} |
||||
|
||||
sort.Slice(users, func(i, j int) bool { |
||||
return users[i].CreatedAt.Before(users[j].CreatedAt) |
||||
}) |
||||
|
||||
return users |
||||
} |
||||
|
||||
func getUserFromRow(row *sql.Row) *User { |
||||
var id string |
||||
var displayName string |
||||
var displayColor int |
||||
var createdAt time.Time |
||||
var disabledAt *time.Time |
||||
var previousUsernames string |
||||
var userNameChangedAt *time.Time |
||||
var scopesString *string |
||||
|
||||
if err := row.Scan(&id, &displayName, &displayColor, &createdAt, &disabledAt, &previousUsernames, &userNameChangedAt, &scopesString); err != nil { |
||||
return nil |
||||
} |
||||
|
||||
var scopes []string |
||||
if scopesString != nil { |
||||
scopes = strings.Split(*scopesString, ",") |
||||
} |
||||
|
||||
return &User{ |
||||
ID: id, |
||||
DisplayName: displayName, |
||||
DisplayColor: displayColor, |
||||
CreatedAt: createdAt, |
||||
DisabledAt: disabledAt, |
||||
PreviousNames: strings.Split(previousUsernames, ","), |
||||
NameChangedAt: userNameChangedAt, |
||||
Scopes: scopes, |
||||
} |
||||
} |
@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
|
||||
package models |
||||
|
||||
import ( |
||||
"time" |
||||
) |
||||
|
||||
// ExternalAPIUser represents a single 3rd party integration that uses an access token.
|
||||
// This struct mostly matches the User struct so they can be used interchangeably.
|
||||
type ExternalAPIUser struct { |
||||
CreatedAt time.Time `json:"createdAt"` |
||||
LastUsedAt *time.Time `json:"lastUsedAt,omitempty"` |
||||
ID string `json:"id"` |
||||
AccessToken string `json:"accessToken"` |
||||
DisplayName string `json:"displayName"` |
||||
Type string `json:"type,omitempty"` // Should be API
|
||||
Scopes []string `json:"scopes"` |
||||
DisplayColor int `json:"displayColor"` |
||||
IsBot bool `json:"isBot"` |
||||
} |
@ -0,0 +1,36 @@
@@ -0,0 +1,36 @@
|
||||
package models |
||||
|
||||
import ( |
||||
"time" |
||||
|
||||
"github.com/owncast/owncast/utils" |
||||
) |
||||
|
||||
const ( |
||||
moderatorScopeKey = "MODERATOR" |
||||
) |
||||
|
||||
type User struct { |
||||
CreatedAt time.Time `json:"createdAt"` |
||||
DisabledAt *time.Time `json:"disabledAt,omitempty"` |
||||
NameChangedAt *time.Time `json:"nameChangedAt,omitempty"` |
||||
AuthenticatedAt *time.Time `json:"-"` |
||||
ID string `json:"id"` |
||||
DisplayName string `json:"displayName"` |
||||
PreviousNames []string `json:"previousNames"` |
||||
Scopes []string `json:"scopes,omitempty"` |
||||
DisplayColor int `json:"displayColor"` |
||||
IsBot bool `json:"isBot"` |
||||
Authenticated bool `json:"authenticated"` |
||||
} |
||||
|
||||
// IsEnabled will return if this single user is enabled.
|
||||
func (u *User) IsEnabled() bool { |
||||
return u.DisabledAt == nil |
||||
} |
||||
|
||||
// IsModerator will return if the user has moderation privileges.
|
||||
func (u *User) IsModerator() bool { |
||||
_, hasModerationScope := utils.FindInSlice(u.Scopes, moderatorScopeKey) |
||||
return hasModerationScope |
||||
} |
@ -0,0 +1,770 @@
@@ -0,0 +1,770 @@
|
||||
package storage |
||||
|
||||
import ( |
||||
"context" |
||||
"database/sql" |
||||
"fmt" |
||||
"sort" |
||||
"strings" |
||||
"time" |
||||
|
||||
"github.com/owncast/owncast/config" |
||||
"github.com/owncast/owncast/core/data" |
||||
"github.com/owncast/owncast/db" |
||||
"github.com/owncast/owncast/models" |
||||
"github.com/owncast/owncast/utils" |
||||
"github.com/pkg/errors" |
||||
"github.com/teris-io/shortid" |
||||
|
||||
log "github.com/sirupsen/logrus" |
||||
) |
||||
|
||||
type UserRepository interface { |
||||
ChangeUserColor(userID string, color int) error |
||||
ChangeUsername(userID string, username string) error |
||||
CreateAnonymousUser(displayName string) (*models.User, string, error) |
||||
DeleteExternalAPIUser(token string) error |
||||
GetDisabledUsers() []*models.User |
||||
GetExternalAPIUser() ([]models.ExternalAPIUser, error) |
||||
GetExternalAPIUserForAccessTokenAndScope(token string, scope string) (*models.ExternalAPIUser, error) |
||||
GetModeratorUsers() []*models.User |
||||
GetUserByID(id string) *models.User |
||||
GetUserByToken(token string) *models.User |
||||
InsertExternalAPIUser(token string, name string, color int, scopes []string) error |
||||
IsDisplayNameAvailable(displayName string) (bool, error) |
||||
SetAccessTokenToOwner(token, userID string) error |
||||
SetEnabled(userID string, enabled bool) error |
||||
SetModerator(userID string, isModerator bool) error |
||||
SetUserAsAuthenticated(userID string) error |
||||
HasValidScopes(scopes []string) bool |
||||
} |
||||
|
||||
type SqlUserRepository struct { |
||||
datastore *data.Datastore |
||||
} |
||||
|
||||
// NOTE: This is temporary during the transition period.
|
||||
var temporaryGlobalInstance UserRepository |
||||
|
||||
// GetUserRepository will return the user repository.
|
||||
func GetUserRepository() UserRepository { |
||||
if temporaryGlobalInstance == nil { |
||||
i := NewUserRepository(data.GetDatastore()) |
||||
temporaryGlobalInstance = i |
||||
} |
||||
return temporaryGlobalInstance |
||||
} |
||||
|
||||
const ( |
||||
// ScopeCanSendChatMessages will allow sending chat messages as itself.
|
||||
ScopeCanSendChatMessages = "CAN_SEND_MESSAGES" |
||||
// ScopeCanSendSystemMessages will allow sending chat messages as the system.
|
||||
ScopeCanSendSystemMessages = "CAN_SEND_SYSTEM_MESSAGES" |
||||
// ScopeHasAdminAccess will allow performing administrative actions on the server.
|
||||
ScopeHasAdminAccess = "HAS_ADMIN_ACCESS" |
||||
|
||||
moderatorScopeKey = "MODERATOR" |
||||
minSuggestedUsernamePoolLength = 10 |
||||
) |
||||
|
||||
// User represents a single chat user.
|
||||
|
||||
// SetupUsers will perform the initial initialization of the user package.
|
||||
func NewUserRepository(datastore *data.Datastore) UserRepository { |
||||
r := &SqlUserRepository{ |
||||
datastore: datastore, |
||||
} |
||||
|
||||
return r |
||||
} |
||||
|
||||
func (u *SqlUserRepository) generateDisplayName() string { |
||||
suggestedUsernamesList := data.GetSuggestedUsernamesList() |
||||
|
||||
if len(suggestedUsernamesList) >= minSuggestedUsernamePoolLength { |
||||
index := utils.RandomIndex(len(suggestedUsernamesList)) |
||||
return suggestedUsernamesList[index] |
||||
} else { |
||||
return utils.GeneratePhrase() |
||||
} |
||||
} |
||||
|
||||
// CreateAnonymousUser will create a new anonymous user with the provided display name.
|
||||
func (r *SqlUserRepository) CreateAnonymousUser(displayName string) (*models.User, string, error) { |
||||
// Try to assign a name that was requested.
|
||||
if displayName != "" { |
||||
// If name isn't available then generate a random one.
|
||||
if available, _ := r.IsDisplayNameAvailable(displayName); !available { |
||||
displayName = r.generateDisplayName() |
||||
} |
||||
} else { |
||||
displayName = r.generateDisplayName() |
||||
} |
||||
|
||||
displayColor := utils.GenerateRandomDisplayColor(config.MaxUserColor) |
||||
|
||||
id := shortid.MustGenerate() |
||||
user := &models.User{ |
||||
ID: id, |
||||
DisplayName: displayName, |
||||
DisplayColor: displayColor, |
||||
CreatedAt: time.Now(), |
||||
} |
||||
|
||||
// Create new user.
|
||||
if err := r.create(user); err != nil { |
||||
return nil, "", err |
||||
} |
||||
|
||||
// Assign it an access token.
|
||||
accessToken, err := utils.GenerateAccessToken() |
||||
if err != nil { |
||||
log.Errorln("Unable to create access token for new user") |
||||
return nil, "", err |
||||
} |
||||
if err := r.addAccessTokenForUser(accessToken, id); err != nil { |
||||
return nil, "", errors.Wrap(err, "unable to save access token for new user") |
||||
} |
||||
|
||||
return user, accessToken, nil |
||||
} |
||||
|
||||
// IsDisplayNameAvailable will check if the proposed name is available for use.
|
||||
func (r *SqlUserRepository) IsDisplayNameAvailable(displayName string) (bool, error) { |
||||
if available, err := r.datastore.GetQueries().IsDisplayNameAvailable(context.Background(), displayName); err != nil { |
||||
return false, errors.Wrap(err, "unable to check if display name is available") |
||||
} else if available != 0 { |
||||
return false, nil |
||||
} |
||||
|
||||
return true, nil |
||||
} |
||||
|
||||
// ChangeUsername will change the user associated to userID from one display name to another.
|
||||
func (r *SqlUserRepository) ChangeUsername(userID string, username string) error { |
||||
r.datastore.DbLock.Lock() |
||||
defer r.datastore.DbLock.Unlock() |
||||
|
||||
if err := r.datastore.GetQueries().ChangeDisplayName(context.Background(), db.ChangeDisplayNameParams{ |
||||
DisplayName: username, |
||||
ID: userID, |
||||
PreviousNames: sql.NullString{String: fmt.Sprintf(",%s", username), Valid: true}, |
||||
NamechangedAt: sql.NullTime{Time: time.Now(), Valid: true}, |
||||
}); err != nil { |
||||
return errors.Wrap(err, "unable to change display name") |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// ChangeUserColor will change the user associated to userID from one display name to another.
|
||||
func (r *SqlUserRepository) ChangeUserColor(userID string, color int) error { |
||||
r.datastore.DbLock.Lock() |
||||
defer r.datastore.DbLock.Unlock() |
||||
|
||||
if err := r.datastore.GetQueries().ChangeDisplayColor(context.Background(), db.ChangeDisplayColorParams{ |
||||
DisplayColor: int32(color), |
||||
ID: userID, |
||||
}); err != nil { |
||||
return errors.Wrap(err, "unable to change display color") |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func (r *SqlUserRepository) addAccessTokenForUser(accessToken, userID string) error { |
||||
return r.datastore.GetQueries().AddAccessTokenForUser(context.Background(), db.AddAccessTokenForUserParams{ |
||||
Token: accessToken, |
||||
UserID: userID, |
||||
}) |
||||
} |
||||
|
||||
func (r *SqlUserRepository) create(user *models.User) error { |
||||
r.datastore.DbLock.Lock() |
||||
defer r.datastore.DbLock.Unlock() |
||||
|
||||
tx, err := r.datastore.DB.Begin() |
||||
if err != nil { |
||||
log.Debugln(err) |
||||
} |
||||
defer func() { |
||||
_ = tx.Rollback() |
||||
}() |
||||
|
||||
stmt, err := tx.Prepare("INSERT INTO users(id, display_name, display_color, previous_names, created_at) values(?, ?, ?, ?, ?)") |
||||
if err != nil { |
||||
log.Debugln(err) |
||||
} |
||||
defer stmt.Close() |
||||
|
||||
_, err = stmt.Exec(user.ID, user.DisplayName, user.DisplayColor, user.DisplayName, user.CreatedAt) |
||||
if err != nil { |
||||
log.Errorln("error creating new user", err) |
||||
return err |
||||
} |
||||
|
||||
return tx.Commit() |
||||
} |
||||
|
||||
// SetEnabled will set the enabled status of a single user by ID.
|
||||
func (r *SqlUserRepository) SetEnabled(userID string, enabled bool) error { |
||||
r.datastore.DbLock.Lock() |
||||
defer r.datastore.DbLock.Unlock() |
||||
|
||||
tx, err := r.datastore.DB.Begin() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
defer tx.Rollback() //nolint
|
||||
|
||||
var stmt *sql.Stmt |
||||
if !enabled { |
||||
stmt, err = tx.Prepare("UPDATE users SET disabled_at=DATETIME('now', 'localtime') WHERE id IS ?") |
||||
} else { |
||||
stmt, err = tx.Prepare("UPDATE users SET disabled_at=null WHERE id IS ?") |
||||
} |
||||
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
defer stmt.Close() |
||||
|
||||
if _, err := stmt.Exec(userID); err != nil { |
||||
return err |
||||
} |
||||
|
||||
return tx.Commit() |
||||
} |
||||
|
||||
// GetUserByToken will return a user by an access token.
|
||||
func (r *SqlUserRepository) GetUserByToken(token string) *models.User { |
||||
u, err := r.datastore.GetQueries().GetUserByAccessToken(context.Background(), token) |
||||
if err != nil { |
||||
return nil |
||||
} |
||||
|
||||
var scopes []string |
||||
if u.Scopes.Valid { |
||||
scopes = strings.Split(u.Scopes.String, ",") |
||||
} |
||||
|
||||
var disabledAt *time.Time |
||||
if u.DisabledAt.Valid { |
||||
disabledAt = &u.DisabledAt.Time |
||||
} |
||||
|
||||
var authenticatedAt *time.Time |
||||
if u.AuthenticatedAt.Valid { |
||||
authenticatedAt = &u.AuthenticatedAt.Time |
||||
} |
||||
|
||||
return &models.User{ |
||||
ID: u.ID, |
||||
DisplayName: u.DisplayName, |
||||
DisplayColor: int(u.DisplayColor), |
||||
CreatedAt: u.CreatedAt.Time, |
||||
DisabledAt: disabledAt, |
||||
PreviousNames: strings.Split(u.PreviousNames.String, ","), |
||||
NameChangedAt: &u.NamechangedAt.Time, |
||||
AuthenticatedAt: authenticatedAt, |
||||
Authenticated: authenticatedAt != nil, |
||||
Scopes: scopes, |
||||
} |
||||
} |
||||
|
||||
// SetAccessTokenToOwner will reassign an access token to be owned by a
|
||||
// different user. Used for logging in with external auth.
|
||||
func (r *SqlUserRepository) SetAccessTokenToOwner(token, userID string) error { |
||||
return r.datastore.GetQueries().SetAccessTokenToOwner(context.Background(), db.SetAccessTokenToOwnerParams{ |
||||
UserID: userID, |
||||
Token: token, |
||||
}) |
||||
} |
||||
|
||||
// SetUserAsAuthenticated will mark that a user has been authenticated
|
||||
// in some way.
|
||||
func (r *SqlUserRepository) SetUserAsAuthenticated(userID string) error { |
||||
return errors.Wrap(r.datastore.GetQueries().SetUserAsAuthenticated(context.Background(), userID), "unable to set user as authenticated") |
||||
} |
||||
|
||||
// SetModerator will add or remove moderator status for a single user by ID.
|
||||
func (r *SqlUserRepository) SetModerator(userID string, isModerator bool) error { |
||||
if isModerator { |
||||
return r.addScopeToUser(userID, moderatorScopeKey) |
||||
} |
||||
|
||||
return r.removeScopeFromUser(userID, moderatorScopeKey) |
||||
} |
||||
|
||||
func (r *SqlUserRepository) addScopeToUser(userID string, scope string) error { |
||||
u := r.GetUserByID(userID) |
||||
if u == nil { |
||||
return errors.New("user not found when modifying scope") |
||||
} |
||||
|
||||
scopesString := u.Scopes |
||||
scopes := utils.StringSliceToMap(scopesString) |
||||
scopes[scope] = true |
||||
|
||||
scopesSlice := utils.StringMapKeys(scopes) |
||||
|
||||
return r.setScopesOnUser(userID, scopesSlice) |
||||
} |
||||
|
||||
func (r *SqlUserRepository) removeScopeFromUser(userID string, scope string) error { |
||||
u := r.GetUserByID(userID) |
||||
scopesString := u.Scopes |
||||
scopes := utils.StringSliceToMap(scopesString) |
||||
delete(scopes, scope) |
||||
|
||||
scopesSlice := utils.StringMapKeys(scopes) |
||||
|
||||
return r.setScopesOnUser(userID, scopesSlice) |
||||
} |
||||
|
||||
func (r *SqlUserRepository) setScopesOnUser(userID string, scopes []string) error { |
||||
r.datastore.DbLock.Lock() |
||||
defer r.datastore.DbLock.Unlock() |
||||
|
||||
tx, err := r.datastore.DB.Begin() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
defer tx.Rollback() //nolint
|
||||
|
||||
scopesSliceString := strings.TrimSpace(strings.Join(scopes, ",")) |
||||
stmt, err := tx.Prepare("UPDATE users SET scopes=? WHERE id IS ?") |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
defer stmt.Close() |
||||
|
||||
var val *string |
||||
if scopesSliceString == "" { |
||||
val = nil |
||||
} else { |
||||
val = &scopesSliceString |
||||
} |
||||
|
||||
if _, err := stmt.Exec(val, userID); err != nil { |
||||
return err |
||||
} |
||||
|
||||
return tx.Commit() |
||||
} |
||||
|
||||
// GetUserByID will return a user by a user ID.
|
||||
func (r *SqlUserRepository) GetUserByID(id string) *models.User { |
||||
r.datastore.DbLock.Lock() |
||||
defer r.datastore.DbLock.Unlock() |
||||
|
||||
query := "SELECT id, display_name, display_color, created_at, disabled_at, previous_names, namechanged_at, scopes FROM users WHERE id = ?" |
||||
row := r.datastore.DB.QueryRow(query, id) |
||||
if row == nil { |
||||
log.Errorln(row) |
||||
return nil |
||||
} |
||||
return r.getUserFromRow(row) |
||||
} |
||||
|
||||
// GetDisabledUsers will return back all the currently disabled users that are not API users.
|
||||
func (r *SqlUserRepository) GetDisabledUsers() []*models.User { |
||||
query := "SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at FROM users WHERE disabled_at IS NOT NULL AND type IS NOT 'API'" |
||||
|
||||
rows, err := r.datastore.DB.Query(query) |
||||
if err != nil { |
||||
log.Errorln(err) |
||||
return nil |
||||
} |
||||
defer rows.Close() |
||||
|
||||
users := r.getUsersFromRows(rows) |
||||
|
||||
sort.Slice(users, func(i, j int) bool { |
||||
return users[i].DisabledAt.Before(*users[j].DisabledAt) |
||||
}) |
||||
|
||||
return users |
||||
} |
||||
|
||||
// GetModeratorUsers will return a list of users with moderator access.
|
||||
func (r *SqlUserRepository) GetModeratorUsers() []*models.User { |
||||
query := `SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at FROM ( |
||||
WITH RECURSIVE split(id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at, scope, rest) AS ( |
||||
SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at, '', scopes || ',' FROM users |
||||
UNION ALL |
||||
SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at, |
||||
substr(rest, 0, instr(rest, ',')), |
||||
substr(rest, instr(rest, ',')+1) |
||||
FROM split |
||||
WHERE rest <> '') |
||||
SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at, scope |
||||
FROM split |
||||
WHERE scope <> '' |
||||
ORDER BY created_at |
||||
) AS token WHERE token.scope = ?` |
||||
|
||||
rows, err := r.datastore.DB.Query(query, moderatorScopeKey) |
||||
if err != nil { |
||||
log.Errorln(err) |
||||
return nil |
||||
} |
||||
defer rows.Close() |
||||
|
||||
users := r.getUsersFromRows(rows) |
||||
|
||||
return users |
||||
} |
||||
|
||||
func (r *SqlUserRepository) getUsersFromRows(rows *sql.Rows) []*models.User { |
||||
users := make([]*models.User, 0) |
||||
|
||||
for rows.Next() { |
||||
var id string |
||||
var displayName string |
||||
var displayColor int |
||||
var createdAt time.Time |
||||
var disabledAt *time.Time |
||||
var previousUsernames string |
||||
var userNameChangedAt *time.Time |
||||
var scopesString *string |
||||
|
||||
if err := rows.Scan(&id, &displayName, &scopesString, &displayColor, &createdAt, &disabledAt, &previousUsernames, &userNameChangedAt); err != nil { |
||||
log.Errorln("error creating collection of users from results", err) |
||||
return nil |
||||
} |
||||
|
||||
var scopes []string |
||||
if scopesString != nil { |
||||
scopes = strings.Split(*scopesString, ",") |
||||
} |
||||
|
||||
user := &models.User{ |
||||
ID: id, |
||||
DisplayName: displayName, |
||||
DisplayColor: displayColor, |
||||
CreatedAt: createdAt, |
||||
DisabledAt: disabledAt, |
||||
PreviousNames: strings.Split(previousUsernames, ","), |
||||
NameChangedAt: userNameChangedAt, |
||||
Scopes: scopes, |
||||
} |
||||
users = append(users, user) |
||||
} |
||||
|
||||
sort.Slice(users, func(i, j int) bool { |
||||
return users[i].CreatedAt.Before(users[j].CreatedAt) |
||||
}) |
||||
|
||||
return users |
||||
} |
||||
|
||||
func (r *SqlUserRepository) getUserFromRow(row *sql.Row) *models.User { |
||||
var id string |
||||
var displayName string |
||||
var displayColor int |
||||
var createdAt time.Time |
||||
var disabledAt *time.Time |
||||
var previousUsernames string |
||||
var userNameChangedAt *time.Time |
||||
var scopesString *string |
||||
|
||||
if err := row.Scan(&id, &displayName, &displayColor, &createdAt, &disabledAt, &previousUsernames, &userNameChangedAt, &scopesString); err != nil { |
||||
return nil |
||||
} |
||||
|
||||
var scopes []string |
||||
if scopesString != nil { |
||||
scopes = strings.Split(*scopesString, ",") |
||||
} |
||||
|
||||
return &models.User{ |
||||
ID: id, |
||||
DisplayName: displayName, |
||||
DisplayColor: displayColor, |
||||
CreatedAt: createdAt, |
||||
DisabledAt: disabledAt, |
||||
PreviousNames: strings.Split(previousUsernames, ","), |
||||
NameChangedAt: userNameChangedAt, |
||||
Scopes: scopes, |
||||
} |
||||
} |
||||
|
||||
// InsertExternalAPIUser will add a new API user to the database.
|
||||
func (r *SqlUserRepository) InsertExternalAPIUser(token string, name string, color int, scopes []string) error { |
||||
log.Traceln("Adding new API user") |
||||
|
||||
r.datastore.DbLock.Lock() |
||||
defer r.datastore.DbLock.Unlock() |
||||
|
||||
scopesString := strings.Join(scopes, ",") |
||||
id := shortid.MustGenerate() |
||||
|
||||
tx, err := r.datastore.DB.Begin() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
stmt, err := tx.Prepare("INSERT INTO users(id, display_name, display_color, scopes, type, previous_names) values(?, ?, ?, ?, ?, ?)") |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer stmt.Close() |
||||
|
||||
if _, err = stmt.Exec(id, name, color, scopesString, "API", name); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if err = tx.Commit(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if err := r.addAccessTokenForUser(token, id); err != nil { |
||||
return errors.Wrap(err, "unable to save access token for new external api user") |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// DeleteExternalAPIUser will delete a token from the database.
|
||||
func (r *SqlUserRepository) DeleteExternalAPIUser(token string) error { |
||||
log.Traceln("Deleting access token") |
||||
|
||||
r.datastore.DbLock.Lock() |
||||
defer r.datastore.DbLock.Unlock() |
||||
|
||||
tx, err := r.datastore.DB.Begin() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
stmt, err := tx.Prepare("UPDATE users SET disabled_at = CURRENT_TIMESTAMP WHERE id = (SELECT user_id FROM user_access_tokens WHERE token = ?)") |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer stmt.Close() |
||||
|
||||
result, err := stmt.Exec(token) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if rowsDeleted, _ := result.RowsAffected(); rowsDeleted == 0 { |
||||
tx.Rollback() //nolint
|
||||
return errors.New(token + " not found") |
||||
} |
||||
|
||||
if err = tx.Commit(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// GetExternalAPIUserForAccessTokenAndScope will determine if a specific token has access to perform a scoped action.
|
||||
func (r *SqlUserRepository) GetExternalAPIUserForAccessTokenAndScope(token string, scope string) (*models.ExternalAPIUser, error) { |
||||
// This will split the scopes from comma separated to individual rows
|
||||
// so we can efficiently find if a token supports a single scope.
|
||||
// This is SQLite specific, so if we ever support other database
|
||||
// backends we need to support other methods.
|
||||
query := `SELECT |
||||
id, |
||||
scopes, |
||||
display_name, |
||||
display_color, |
||||
created_at, |
||||
last_used |
||||
FROM |
||||
user_access_tokens |
||||
INNER JOIN ( |
||||
WITH RECURSIVE split( |
||||
id, |
||||
scopes, |
||||
display_name, |
||||
display_color, |
||||
created_at, |
||||
last_used, |
||||
disabled_at, |
||||
scope, |
||||
rest |
||||
) AS ( |
||||
SELECT |
||||
id, |
||||
scopes, |
||||
display_name, |
||||
display_color, |
||||
created_at, |
||||
last_used, |
||||
disabled_at, |
||||
'', |
||||
scopes || ',' |
||||
FROM |
||||
users AS u |
||||
UNION ALL |
||||
SELECT |
||||
id, |
||||
scopes, |
||||
display_name, |
||||
display_color, |
||||
created_at, |
||||
last_used, |
||||
disabled_at, |
||||
substr(rest, 0, instr(rest, ',')), |
||||
substr(rest, instr(rest, ',') + 1) |
||||
FROM |
||||
split |
||||
WHERE |
||||
rest <> '' |
||||
) |
||||
SELECT |
||||
id, |
||||
display_name, |
||||
display_color, |
||||
created_at, |
||||
last_used, |
||||
disabled_at, |
||||
scopes, |
||||
scope |
||||
FROM |
||||
split |
||||
WHERE |
||||
scope <> '' |
||||
) ON user_access_tokens.user_id = id |
||||
WHERE |
||||
disabled_at IS NULL |
||||
AND token = ? |
||||
AND scope = ?;` |
||||
|
||||
row := r.datastore.DB.QueryRow(query, token, scope) |
||||
integration, err := r.makeExternalAPIUserFromRow(row) |
||||
|
||||
return integration, err |
||||
} |
||||
|
||||
// GetIntegrationNameForAccessToken will return the integration name associated with a specific access token.
|
||||
func (r *SqlUserRepository) GetIntegrationNameForAccessToken(token string) *string { |
||||
name, err := r.datastore.GetQueries().GetUserDisplayNameByToken(context.Background(), token) |
||||
if err != nil { |
||||
return nil |
||||
} |
||||
|
||||
return &name |
||||
} |
||||
|
||||
// GetExternalAPIUser will return all API users with access tokens.
|
||||
func (r *SqlUserRepository) GetExternalAPIUser() ([]models.ExternalAPIUser, error) { //nolint
|
||||
query := "SELECT id, token, display_name, display_color, scopes, created_at, last_used FROM users, user_access_tokens WHERE user_access_tokens.user_id = id AND type IS 'API' AND disabled_at IS NULL" |
||||
|
||||
rows, err := r.datastore.DB.Query(query) |
||||
if err != nil { |
||||
return []models.ExternalAPIUser{}, err |
||||
} |
||||
defer rows.Close() |
||||
|
||||
integrations, err := r.makeExternalAPIUsersFromRows(rows) |
||||
|
||||
return integrations, err |
||||
} |
||||
|
||||
// SetExternalAPIUserAccessTokenAsUsed will update the last used timestamp for a token.
|
||||
func (r *SqlUserRepository) SetExternalAPIUserAccessTokenAsUsed(token string) error { |
||||
tx, err := r.datastore.DB.Begin() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
stmt, err := tx.Prepare("UPDATE users SET last_used = CURRENT_TIMESTAMP WHERE id = (SELECT user_id FROM user_access_tokens WHERE token = ?)") |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer stmt.Close() |
||||
|
||||
if _, err := stmt.Exec(token); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if err = tx.Commit(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func (r *SqlUserRepository) makeExternalAPIUserFromRow(row *sql.Row) (*models.ExternalAPIUser, error) { |
||||
var id string |
||||
var displayName string |
||||
var displayColor int |
||||
var scopes string |
||||
var createdAt time.Time |
||||
var lastUsedAt *time.Time |
||||
|
||||
err := row.Scan(&id, &scopes, &displayName, &displayColor, &createdAt, &lastUsedAt) |
||||
if err != nil { |
||||
log.Debugln("unable to convert row to api user", err) |
||||
return nil, err |
||||
} |
||||
|
||||
integration := models.ExternalAPIUser{ |
||||
ID: id, |
||||
DisplayName: displayName, |
||||
DisplayColor: displayColor, |
||||
CreatedAt: createdAt, |
||||
Scopes: strings.Split(scopes, ","), |
||||
LastUsedAt: lastUsedAt, |
||||
} |
||||
|
||||
return &integration, nil |
||||
} |
||||
|
||||
func (r *SqlUserRepository) makeExternalAPIUsersFromRows(rows *sql.Rows) ([]models.ExternalAPIUser, error) { |
||||
integrations := make([]models.ExternalAPIUser, 0) |
||||
|
||||
for rows.Next() { |
||||
var id string |
||||
var accessToken string |
||||
var displayName string |
||||
var displayColor int |
||||
var scopes string |
||||
var createdAt time.Time |
||||
var lastUsedAt *time.Time |
||||
|
||||
err := rows.Scan(&id, &accessToken, &displayName, &displayColor, &scopes, &createdAt, &lastUsedAt) |
||||
if err != nil { |
||||
log.Errorln(err) |
||||
return nil, err |
||||
} |
||||
|
||||
integration := models.ExternalAPIUser{ |
||||
ID: id, |
||||
AccessToken: accessToken, |
||||
DisplayName: displayName, |
||||
DisplayColor: displayColor, |
||||
CreatedAt: createdAt, |
||||
Scopes: strings.Split(scopes, ","), |
||||
LastUsedAt: lastUsedAt, |
||||
IsBot: true, |
||||
} |
||||
integrations = append(integrations, integration) |
||||
} |
||||
|
||||
return integrations, nil |
||||
} |
||||
|
||||
// HasValidScopes will verify that all the scopes provided are valid.
|
||||
func (r *SqlUserRepository) HasValidScopes(scopes []string) bool { |
||||
// For a scope to be seen as "valid" it must live in this slice.
|
||||
validAccessTokenScopes := []string{ |
||||
ScopeCanSendChatMessages, |
||||
ScopeCanSendSystemMessages, |
||||
ScopeHasAdminAccess, |
||||
} |
||||
|
||||
for _, scope := range scopes { |
||||
_, foundInSlice := utils.FindInSlice(validAccessTokenScopes, scope) |
||||
if !foundInSlice { |
||||
return false |
||||
} |
||||
} |
||||
return true |
||||
} |
Loading…
Reference in new issue