2021-07-20 04:22:29 +02:00
package user
import (
2022-04-21 23:55:26 +02:00
"context"
2021-07-20 04:22:29 +02:00
"database/sql"
"strings"
"time"
"github.com/owncast/owncast/utils"
2022-04-21 23:55:26 +02:00
"github.com/pkg/errors"
2021-07-20 04:22:29 +02:00
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 {
2021-09-12 09:18:15 +02:00
ID string ` json:"id" `
2021-07-20 04:22:29 +02:00
AccessToken string ` json:"accessToken" `
DisplayName string ` json:"displayName" `
DisplayColor int ` json:"displayColor" `
CreatedAt time . Time ` json:"createdAt" `
Scopes [ ] string ` json:"scopes" `
Type string ` json:"type,omitempty" ` // Should be API
LastUsedAt * time . Time ` json:"lastUsedAt,omitempty" `
2022-03-07 05:09:55 +01:00
IsBot bool ` json:"isBot" `
2021-07-20 04:22:29 +02:00
}
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 ,
}
2021-09-12 09:18:15 +02:00
// InsertExternalAPIUser will add a new API user to the database.
2021-07-20 04:22:29 +02:00
func InsertExternalAPIUser ( token string , name string , color int , scopes [ ] string ) error {
2022-02-26 00:22:52 +01:00
log . Traceln ( "Adding new API user" )
2021-07-20 04:22:29 +02:00
_datastore . DbLock . Lock ( )
defer _datastore . DbLock . Unlock ( )
scopesString := strings . Join ( scopes , "," )
id := shortid . MustGenerate ( )
tx , err := _datastore . DB . Begin ( )
if err != nil {
return err
}
2022-04-21 23:55:26 +02:00
stmt , err := tx . Prepare ( "INSERT INTO users(id, display_name, display_color, scopes, type, previous_names) values(?, ?, ?, ?, ?, ?)" )
2021-07-20 04:22:29 +02:00
if err != nil {
return err
}
defer stmt . Close ( )
2022-04-21 23:55:26 +02:00
if _ , err = stmt . Exec ( id , name , color , scopesString , "API" , name ) ; err != nil {
2021-07-20 04:22:29 +02:00
return err
}
if err = tx . Commit ( ) ; err != nil {
return err
}
2022-04-21 23:55:26 +02:00
if err := addAccessTokenForUser ( token , id ) ; err != nil {
return errors . Wrap ( err , "unable to save access token for new external api user" )
}
2021-07-20 04:22:29 +02:00
return nil
}
// DeleteExternalAPIUser will delete a token from the database.
func DeleteExternalAPIUser ( token string ) error {
2022-02-26 00:22:52 +01:00
log . Traceln ( "Deleting access token" )
2021-07-20 04:22:29 +02:00
_datastore . DbLock . Lock ( )
defer _datastore . DbLock . Unlock ( )
tx , err := _datastore . DB . Begin ( )
if err != nil {
return err
}
2022-04-21 23:55:26 +02:00
stmt , err := tx . Prepare ( "UPDATE users SET disabled_at = CURRENT_TIMESTAMP WHERE id = (SELECT user_id FROM user_access_tokens WHERE token = ?)" )
2021-07-20 04:22:29 +02:00
if err != nil {
return err
}
defer stmt . Close ( )
2022-04-21 23:55:26 +02:00
result , err := stmt . Exec ( token )
2021-07-20 04:22:29 +02:00
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.
2022-04-21 23:55:26 +02:00
query := ` SELECT id , scopes , display_name , display_color , created_at , last_used FROM user_access_tokens , (
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
2021-07-20 04:22:29 +02:00
UNION ALL
2022-04-21 23:55:26 +02:00
SELECT id , scopes , display_name , display_color , created_at , last_used , disabled_at ,
2021-07-20 04:22:29 +02:00
substr ( rest , 0 , instr ( rest , ',' ) ) ,
substr ( rest , instr ( rest , ',' ) + 1 )
FROM split
WHERE rest < > ' ' )
2022-04-21 23:55:26 +02:00
SELECT id , scopes , display_name , display_color , created_at , last_used , disabled_at , scope
2022-02-26 00:22:52 +01:00
FROM split
2021-07-20 04:22:29 +02:00
WHERE scope < > ' '
2022-04-21 23:55:26 +02:00
ORDER BY scope
) AS token WHERE user_access_tokens . token = ? AND token . scope = ? `
2021-07-20 04:22:29 +02:00
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 {
2022-04-21 23:55:26 +02:00
name , err := _datastore . GetQueries ( ) . GetUserDisplayNameByToken ( context . Background ( ) , token )
2021-07-20 04:22:29 +02:00
if err != nil {
return nil
}
return & name
}
2022-04-21 23:55:26 +02:00
// GetExternalAPIUser will return all API users with access tokens.
2021-07-20 04:22:29 +02:00
func GetExternalAPIUser ( ) ( [ ] ExternalAPIUser , error ) { //nolint
// Get all messages sent within the past day
2022-04-21 23:55:26 +02:00
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"
2021-07-20 04:22:29 +02:00
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
}
2022-04-21 23:55:26 +02:00
// stmt, err := tx.Prepare("UPDATE users SET last_used = CURRENT_TIMESTAMP WHERE access_token = ?")
stmt , err := tx . Prepare ( "UPDATE users SET last_used = CURRENT_TIMESTAMP WHERE id = (SELECT user_id FROM user_access_tokens WHERE token = ?)" )
2021-07-20 04:22:29 +02:00
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
2022-04-21 23:55:26 +02:00
err := row . Scan ( & id , & scopes , & displayName , & displayColor , & createdAt , & lastUsedAt )
2021-07-20 04:22:29 +02:00
if err != nil {
2021-07-28 21:37:26 +02:00
log . Debugln ( "unable to convert row to api user" , err )
2021-07-20 04:22:29 +02:00
return nil , err
}
integration := ExternalAPIUser {
2021-09-12 09:18:15 +02:00
ID : id ,
2021-07-20 04:22:29 +02:00
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 {
2021-09-12 09:18:15 +02:00
ID : id ,
2021-07-20 04:22:29 +02:00
AccessToken : accessToken ,
DisplayName : displayName ,
DisplayColor : displayColor ,
CreatedAt : createdAt ,
Scopes : strings . Split ( scopes , "," ) ,
LastUsedAt : lastUsedAt ,
2022-03-07 05:09:55 +01:00
IsBot : true ,
2021-07-20 04:22:29 +02:00
}
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
}