updating apitokens to be different, removing /api/v1.4 prefix, scopes

This commit is contained in:
its-a-feature
2026-04-18 15:03:54 -05:00
parent 22ef644aea
commit 45c02bd2d4
23 changed files with 463 additions and 306 deletions
+2
View File
@@ -167,3 +167,5 @@ _ReSharper*/
jupyter-docker/jupyter/MythicExamples/.ipynb_checkpoints/
nginx-docker/config/blockips.conf
.gocache
@@ -26,7 +26,7 @@ export function ResponseDisplayScreenshotModal({onClose, images, startIndex}) {
< >
<DialogContent dividers={true} style={{padding: 0}} >
<div style={{height: "calc(80vh)", display: zoom ? "" : "flex", alignItems: zoom ? "" : "flex-start" , background: theme.palette.mode === "dark" ? "rgb(44, 52, 60)" : "grey"}}>
<ImageWithAuth onClick={toggleZoom} src={"/api/v1.4/files/screencaptures/" + images[activeStep]}
<ImageWithAuth onClick={toggleZoom} src={"/screencaptures/" + images[activeStep]}
style={{width: zoom ? "" : "100%", cursor: zoom ? "zoom-out" : "zoom-in", overflow: "auto"}} />
</div>
</DialogContent>
@@ -1535,7 +1535,7 @@ const Top10RecentScreenshotsDashboardElement = ({me, data, editing, removeElemen
</Button>
</MythicTableCell>
<MythicTableCell>
<ImageWithAuth src={"/api/v1.4/files/screencaptures/" + files[activeStep] + "?" + now}
<ImageWithAuth src={"/screencaptures/" + files[activeStep] + "?" + now}
style={{height: "200px", cursor: "pointer"}}
onClick={(e) => onPreviewMedia(e, activeStep)}
/>
@@ -48,7 +48,7 @@ export const PieChartCard = ({
}
const theme = useTheme();
return (
<Paper variant={"elevation"} elevation={1} style={{
<Paper variant={"elevation"} elevation={3} style={{
marginRight: "0.5rem",
width: width,
height: "100%",
@@ -167,7 +167,7 @@ export const GaugeCard = ({data, width = "100%", additionalStyles, innerElement,
}
}
return (
<Paper variant={"elevation"} elevation={1} style={{
<Paper variant={"elevation"} elevation={3} style={{
marginRight: "0.5rem",
width: width,
height: "100%",
@@ -216,7 +216,7 @@ export const CallbackDataCard = ({mainTitle, secondTitle, mainElement, secondary
editing, removeElement}) => {
const theme = useTheme();
return (
<Paper variant={"elevation"} elevation={1} style={{
<Paper variant={"elevation"} elevation={3} style={{
marginRight: "0.5rem",
width: width,
height: "100%",
@@ -251,7 +251,7 @@ export const CallbackDataCard = ({mainTitle, secondTitle, mainElement, secondary
export const TableDataCard = ({title, width = "100%", tableHead, tableBody, editing, removeElement, customizeElement}) => {
const theme = useTheme();
return (
<Paper variant={"elevation"} elevation={1} style={{
<Paper variant={"elevation"} elevation={3} style={{
marginRight: "0.5rem",
width: width,
height: "100%",
@@ -314,7 +314,7 @@ export const LineTimeChartCard = ({data, additionalStyles}) => {
}
};
return (
<Paper variant={"elevation"} elevation={1} style={{
<Paper variant={"elevation"} elevation={3} style={{
marginRight: "0.5rem",
width: "100%",
height: "100%",
@@ -429,7 +429,7 @@ export const LineTimeMultiChartCard = ({data, additionalStyles, colors=normalCol
}
}
return (
<Paper variant={"elevation"} elevation={1} style={{
<Paper variant={"elevation"} elevation={3} style={{
width: "100%",
marginRight: "0.5rem",
height: "100%",
@@ -517,7 +517,7 @@ export const StackedBarChartCard = ({data, labels, title, width="100%", hidden,
}}) => {
const theme = useTheme();
return (
<Paper variant={"elevation"} elevation={1} style={{
<Paper variant={"elevation"} elevation={3} style={{
marginRight: "0.5rem",
width: width,
height: "100%",
@@ -1056,7 +1056,7 @@ function FileMetaScreenshotTableRow(props){
)}
</MythicStyledTableCell>
<MythicStyledTableCell >
<ImageWithAuth src={"/api/v1.4/files/screencaptures/" + props.agent_file_id}
<ImageWithAuth src={"/screencaptures/" + props.agent_file_id}
style={{width: "270px", cursor: "pointer"}} />
{openScreenshot &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openScreenshot}
@@ -32,7 +32,6 @@ import {MythicPageBody} from "../../MythicComponents/MythicPageBody";
export function SettingsOperatorTable(props){
const theme = useTheme();
const [openNew, setOpenNewDialog] = React.useState(false);
const [openNewBot, setOpenNewBotDialog] = React.useState(false);
const [openAPITokenSearch, setOpenAPITokenSearch] = React.useState(false);
-30
View File
@@ -58,36 +58,6 @@ let httpLink = new HttpLink({
});
export const isJWTValid = () => {
let access_token = localStorage.getItem("access_token");
if(!access_token){
let cookie = document.cookie;
if(cookie && cookie !== ""){
let cookies = cookie.split(";");
for(let i = 0; i < cookies.length; i++){
let cookiePieces = cookies[i].split("=");
if(cookiePieces.length !== 2){
console.log("bad number of cookie pieces", "cookie", cookies[i])
} else if(cookiePieces[0].trim() !== "user") {
console.log("unknown cookie", "name", cookiePieces[0].trim());
} else {
try{
console.log("user cookie found, trying to parse");
let cookieString = decodeURIComponent(cookiePieces[1].trim());
let cookieJSON = JSON.parse(atob(cookieString));
if("access_token" in cookieJSON){
successfulLogin(cookieJSON);
restartWebsockets();
access_token = localStorage.getItem("access_token");
}else{
snackActions.warning("Invalid Authentication");
console.log("Error", cookieJSON);
}
}catch(error){
console.log("error processing cookie value", error)
}
}
}
}
}
//console.log("in isJWTValid", "access_token", access_token);
if(access_token){
const decoded_token = jwtDecode(access_token);
@@ -220,7 +220,7 @@ func AddMythicService(service string, removeVolume bool) {
"HASURA_GRAPHQL_LIVE_QUERIES_MULTIPLEXED_REFETCH_INTERVAL=1000",
"HASURA_GRAPHQL_AUTH_HOOK=http://${MYTHIC_SERVER_HOST}:${MYTHIC_SERVER_PORT}/graphql/webhook",
"HASURA_GRAPHQL_AUTH_HOOK_MODE=POST",
"MYTHIC_ACTIONS_URL_BASE=http://${MYTHIC_SERVER_HOST}:${MYTHIC_SERVER_PORT}/api/v1.4",
"MYTHIC_ACTIONS_URL_BASE=http://${MYTHIC_SERVER_HOST}:${MYTHIC_SERVER_PORT}/",
"HASURA_GRAPHQL_CONSOLE_ASSETS_DIR=/srv/console-assets",
}
if mythicEnv.GetString("mythic_docker_networking") == "bridge" {
+136 -159
View File
@@ -3,6 +3,8 @@ package authentication
import (
"errors"
"fmt"
"strings"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt"
"github.com/its-a-feature/Mythic/authentication/mythicjwt"
@@ -11,20 +13,27 @@ import (
"github.com/its-a-feature/Mythic/logging"
"github.com/its-a-feature/Mythic/rabbitmq"
"github.com/its-a-feature/Mythic/utils"
"strings"
)
var (
ErrUnexpectedSigningMethod = errors.New("Unexpected signing method")
ErrGetApiTokenOrBearer = errors.New("Failed to get apitoken or bearer token")
ErrBearerInvalidValue = errors.New("Authorization bearer value is invalid")
ErrMissingAuthorizationBearerToken = errors.New("Missing Authorization Bearer token")
ErrMissingAuthorizationHeader = errors.New("Missing Authorization header")
ErrMissingJWTToken = errors.New("Missing JWT header")
ErrLegacyAPITokenJWTDeprecated = errors.New("Legacy JWT API tokens are no longer accepted. Generate and use a new API token.")
)
const (
ContextKeyClaims = "claims"
ContextKeyAPIToken = "apitoken_struct"
ContextKeyUserID = "user_id"
ContextKeyUsername = "username"
)
var (
SQLGetIDForActiveToken = `SELECT
// LegacyAPITokenJWTCutoff // int64(time.Now().Unix()) for a specific date via iat
LegacyAPITokenJWTCutoff = int64(1776211200) // April 15, 2026
SQLGetIDForActiveToken = `SELECT
apitokens.id, apitokens.operator_id, apitokens.name, apitokens.active, apitokens.token_value, apitokens.deleted,
apitokens.eventstepinstance_id, apitokens.token_type,
operator.username "operator.username",
@@ -32,122 +41,143 @@ var (
operator.current_operation_id "operator.current_operation_id"
FROM apitokens
JOIN operator ON apitokens.operator_id = operator.id
WHERE apitokens.token_value=$1`
WHERE apitokens.token_value=$1
LIMIT 1`
)
func getAPITokenFromDB(c *gin.Context, tokenString string) (databaseStructs.Apitokens, error) {
if cachedToken, ok := c.Get(ContextKeyAPIToken); ok {
if token, ok := cachedToken.(databaseStructs.Apitokens); ok {
return token, nil
}
}
databaseApiToken := databaseStructs.Apitokens{}
hashedTokenValue := mythicjwt.HashAPITokenValue(tokenString)
if err := database.DB.Get(&databaseApiToken, SQLGetIDForActiveToken, hashedTokenValue); err != nil {
logging.LogError(err, "Failed to get apitoken from database", "apitoken", tokenString)
return databaseApiToken, err
}
c.Set(ContextKeyAPIToken, databaseApiToken)
return databaseApiToken, nil
}
func claimsFromAPIToken(databaseApiToken databaseStructs.Apitokens) *mythicjwt.CustomClaims {
claims := mythicjwt.CustomClaims{
UserID: databaseApiToken.OperatorID,
AuthMethod: databaseApiToken.TokenType,
APITokensID: databaseApiToken.ID,
OperationID: int(databaseApiToken.Operator.CurrentOperationID.Int64),
}
if databaseApiToken.EventStepInstanceID.Valid {
claims.EventStepInstanceID = int(databaseApiToken.EventStepInstanceID.Int64)
}
return &claims
}
func looksLikeOpaqueAPIToken(tokenString string) bool {
return strings.HasPrefix(tokenString, mythicjwt.APITokenValuePrefix)
}
func validateAndSetAPITokenContext(c *gin.Context, tokenString string) error {
databaseApiToken, err := getAPITokenFromDB(c, tokenString)
if err != nil {
return err
}
if !databaseApiToken.Active {
go func(token databaseStructs.Apitokens) {
go rabbitmq.SendAllOperationsMessage(fmt.Sprintf("Deactivated APIToken, %s, for user %s (%s) attempted to be used",
token.Name, token.Operator.Username, token.Operator.AccountType),
int(token.Operator.CurrentOperationID.Int64), token.Name+fmt.Sprintf("%d", token.ID)+token.Operator.Username,
database.MESSAGE_LEVEL_API, true)
}(databaseApiToken)
return errors.New("Deactivated APIToken attempted to be used")
}
if databaseApiToken.Deleted {
go func(token databaseStructs.Apitokens) {
go rabbitmq.SendAllOperationsMessage(fmt.Sprintf("Deleted APIToken, \"%s\", for user %s (%s) attempted to be used",
token.Name, token.Operator.Username, token.Operator.AccountType),
int(token.Operator.CurrentOperationID.Int64), token.Name+fmt.Sprintf("%d", token.ID)+token.Operator.Username,
database.MESSAGE_LEVEL_API, true)
}(databaseApiToken)
return errors.New("Deleted APIToken attempted to be used")
}
source := c.GetHeader("MythicSource")
if source == "web" {
logging.LogError(errors.New("web used apitoken"), "API Token used from the web, this is wrong")
return errors.New("web shouldn't use apitokens")
}
if databaseApiToken.EventStepInstanceID.Valid {
c.Request.Header.Set("MythicSource", "eventing")
c.Set("eventstepinstance_id", int(databaseApiToken.EventStepInstanceID.Int64))
} else {
c.Request.Header.Set("MythicSource", "apitoken")
}
c.Set(ContextKeyUserID, databaseApiToken.OperatorID)
c.Set(ContextKeyUsername, databaseApiToken.Operator.Username)
c.Set(ContextKeyAPIToken, databaseApiToken)
c.Set("account", databaseApiToken.Operator.AccountType)
c.Set(ContextKeyClaims, claimsFromAPIToken(databaseApiToken))
return nil
}
func GetClaims(c *gin.Context) (*mythicjwt.CustomClaims, error) {
// just get the claims out of the JWT used for the request
tokenString, err := ExtractToken(c)
if err != nil {
if len(tokenString) == 0 {
tokenString, err = ExtractAPIToken(c)
if err != nil {
return nil, err
}
if len(tokenString) == 0 {
return nil, ErrGetApiTokenOrBearer
}
if existingClaims, exists := c.Get(ContextKeyClaims); exists {
if typedClaims, ok := existingClaims.(*mythicjwt.CustomClaims); ok {
return typedClaims, nil
}
}
token, _, err := new(jwt.Parser).ParseUnverified(tokenString, &mythicjwt.CustomClaims{})
tokenString, err := ExtractToken(c)
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*mythicjwt.CustomClaims); ok {
c.Set("user_id", claims.UserID)
return claims, nil
if looksLikeOpaqueAPIToken(tokenString) {
apiTokenErr := validateAndSetAPITokenContext(c, tokenString)
if apiTokenErr != nil {
logging.LogError(err, "failed to parse jwt")
return nil, apiTokenErr
}
if existingClaims, exists := c.Get(ContextKeyClaims); exists {
if typedClaims, ok := existingClaims.(*mythicjwt.CustomClaims); ok {
return typedClaims, nil
}
}
return nil, errors.New("failed to get claims from validated apitoken")
}
return nil, err
claims := mythicjwt.CustomClaims{}
_, err = jwt.ParseWithClaims(tokenString, &claims, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
logging.LogError(ErrUnexpectedSigningMethod, "signing method", token.Header["alg"])
return nil, fmt.Errorf("%w: %v", ErrUnexpectedSigningMethod, token.Header["alg"])
}
return utils.MythicConfig.JWTSecret, nil
})
if err != nil {
logging.LogError(err, "failed to parse jwt")
return nil, err
}
if claims.IssuedAt < LegacyAPITokenJWTCutoff {
logging.LogError(ErrLegacyAPITokenJWTDeprecated,
"auth_method", claims.AuthMethod,
"issued_at", claims.IssuedAt,
"cutoff", LegacyAPITokenJWTCutoff)
return nil, ErrLegacyAPITokenJWTDeprecated
}
c.Set(ContextKeyUserID, claims.UserID)
c.Set(ContextKeyClaims, &claims)
return &claims, nil
}
func TokenValid(c *gin.Context) error {
tokenString, err := ExtractToken(c)
claims, err := GetClaims(c)
if err != nil {
tokenString, err = ExtractAPIToken(c)
if err != nil {
logging.LogError(err, "Failed to extract apitoken")
return err
}
// we have an apitoken to process
databaseApiToken := databaseStructs.Apitokens{}
if err = database.DB.Get(&databaseApiToken, SQLGetIDForActiveToken, tokenString); err != nil {
logging.LogError(err, "Failed to get apitoken from database", "apitoken", tokenString)
return err
}
if !databaseApiToken.Active {
go func(token databaseStructs.Apitokens) {
go rabbitmq.SendAllOperationsMessage(fmt.Sprintf("Deactivated APIToken, %s, for user %s (%s) attempted to be used",
token.Name, token.Operator.Username, token.Operator.AccountType),
int(token.Operator.CurrentOperationID.Int64), "apitoken usage",
database.MESSAGE_LEVEL_API, true)
}(databaseApiToken)
return errors.New("Deactivated APIToken attempted to be used")
}
if databaseApiToken.Deleted {
go func(token databaseStructs.Apitokens) {
go rabbitmq.SendAllOperationsMessage(fmt.Sprintf("Deleted APIToken, \"%s\", for user %s (%s) attempted to be used",
token.Name, token.Operator.Username, token.Operator.AccountType),
int(token.Operator.CurrentOperationID.Int64), token.Name+fmt.Sprintf("%d", token.ID)+token.Operator.Username,
database.MESSAGE_LEVEL_API, true)
}(databaseApiToken)
return errors.New("Deleted APIToken attempted to be used")
}
source := c.GetHeader("MythicSource")
if source == "web" {
logging.LogError(errors.New("web used apitoken"), "API Token used from the web, this is wrong")
return errors.New("web shouldn't use apitokens")
}
if databaseApiToken.EventStepInstanceID.Valid {
c.Request.Header.Add("MythicSource", "eventing")
c.Set("eventstepinstance_id", int(databaseApiToken.EventStepInstanceID.Int64))
} else {
c.Request.Header.Add("MythicSource", "apitoken")
}
c.Set("apitoken_logging_struct", databaseApiToken)
c.Set("user_id", databaseApiToken.OperatorID)
c.Set("username", databaseApiToken.Operator.Username)
c.Set("apitoken", databaseApiToken.Name)
c.Set("account", databaseApiToken.Operator.AccountType)
/*
if hasura, ok := c.Get("hasura"); ok {
logging.LogInfo("got hasura info", "hasura", hasura)
} else {
logging.LogInfo("no hasura info yet")
}
go func(token databaseStructs.Apitokens) {
go rabbitmq.SendAllOperationsMessage(fmt.Sprintf("APIToken, %s, for user %s (%s) just used",
token.Name, token.Operator.Username, token.Operator.AccountType),
int(token.Operator.CurrentOperationID.Int64), token.TokenValue,
database.MESSAGE_LEVEL_DEBUG)
}(databaseApiToken)
*/
return nil
return err
}
claims := mythicjwt.CustomClaims{}
if len(tokenString) > 0 {
_, err = jwt.ParseWithClaims(tokenString, &claims, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
logging.LogError(ErrUnexpectedSigningMethod, "signing method", token.Header["alg"])
return nil, fmt.Errorf("%w: %v", ErrUnexpectedSigningMethod, token.Header["alg"])
}
return utils.MythicConfig.JWTSecret, nil
})
if err != nil {
logging.LogError(err, "Failed to parse JWT with claims", "JWT", tokenString)
return err
}
operator, err := database.GetUserFromID(claims.UserID)
if err == nil {
c.Set("username", operator.Username)
}
c.Set("user_id", claims.UserID)
return nil
operator, err := database.GetUserFromID(claims.UserID)
if err == nil {
c.Set(ContextKeyUsername, operator.Username)
}
return errors.New("failed to get token")
return nil
}
type HasuraRequest struct {
@@ -162,24 +192,12 @@ type HasuraRequestGraphQL struct {
func ExtractToken(c *gin.Context) (string, error) {
token := c.Request.Header.Get("Authorization")
//logging.LogDebug("got Authorization header", "Authorization", token)
if len(token) == 0 {
token = c.Request.Header.Get("apitoken")
}
if len(token) == 0 {
if c.Request.Method == "POST" {
if _, ok := c.Get("hasura"); !ok {
// don't try to double process, only do this once
/*
var buf bytes.Buffer
tee := io.TeeReader(c.Request.Body, &buf)
body, _ := ioutil.ReadAll(tee)
c.Request.Body = ioutil.NopCloser(&buf)
logging.LogInfo("raw hasura info", "raw body", string(body))
*/
var input HasuraRequest
if err := c.ShouldBindJSON(&input); err != nil {
err := c.ShouldBindJSON(&input)
if err != nil {
logging.LogError(err, "Failed to find hasura request")
return "", ErrMissingAuthorizationHeader
}
@@ -187,7 +205,6 @@ func ExtractToken(c *gin.Context) (string, error) {
c.Request.Header.Add(key, value)
}
c.Set("hasura", input)
//logging.LogInfo("hasura info", "hasura", input)
}
}
token = c.Request.Header.Get("Authorization")
@@ -195,7 +212,6 @@ func ExtractToken(c *gin.Context) (string, error) {
return "", ErrMissingAuthorizationHeader
}
}
bearerPieces := strings.Split(token, " ")
if len(bearerPieces) != 2 {
return "", ErrMissingAuthorizationBearerToken
@@ -203,44 +219,5 @@ func ExtractToken(c *gin.Context) (string, error) {
if len(bearerPieces[1]) > 10 {
return bearerPieces[1], nil
}
return "", ErrBearerInvalidValue
}
func ExtractAPIToken(c *gin.Context) (string, error) {
token := c.Request.Header.Get("apitoken")
if len(token) == 0 {
if c.Request.Method == "POST" {
if _, ok := c.Get("hasura"); !ok {
// don't try to double process, only do this once
/*
var buf bytes.Buffer
tee := io.TeeReader(c.Request.Body, &buf)
body, _ := ioutil.ReadAll(tee)
c.Request.Body = ioutil.NopCloser(&buf)
logging.LogInfo("raw hasura info", "raw body", string(body))
*/
var input HasuraRequest
if err := c.ShouldBindJSON(&input); err != nil {
logging.LogError(err, "Failed to find hasura request")
return "", ErrMissingAuthorizationHeader
}
for key, value := range input.Headers {
c.Request.Header.Add(key, value)
}
c.Set("hasura", input)
//logging.LogInfo("hasura info", "hasura", input)
}
}
token = c.Request.Header.Get("apitoken")
if len(token) == 0 {
if !strings.HasPrefix(c.Request.URL.Path, "/direct/") {
logging.LogError(nil, "[-] No 'apitoken` or 'Authorization: Bearer' token values supplied")
}
return "", ErrMissingJWTToken
}
}
//logging.LogTrace("got apitoken header", "apitoken", token)
return token, nil
}
+4 -33
View File
@@ -20,8 +20,6 @@ func TestTokenValid(t *testing.T) {
}
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
user := databaseStructs.Operator{ID: 123}
access_token, _, _, _ := mythicjwt.GenerateJWT(user, mythicjwt.AUTH_METHOD_USER, 0, 0)
@@ -69,6 +67,8 @@ func TestTokenValid(t *testing.T) {
tt := tt // shadowing
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
req := &http.Request{
Header: make(http.Header),
}
@@ -89,8 +89,6 @@ func TestGetClaims(t *testing.T) {
}
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
user := databaseStructs.Operator{ID: 123}
access_token, _, _, _ := mythicjwt.GenerateJWT(user, mythicjwt.AUTH_METHOD_USER, 0, 0)
@@ -127,33 +125,6 @@ func TestGetClaims(t *testing.T) {
want: nil,
wantErr: true,
},
{
name: "Valid apitoken Claim",
args: args{
header: map[string][]string{
"Apitoken": {access_token}, // "Apitoken" and not "apitoken" here because Go uses the canonical form (first letter of the word and after hyphens as uppercase)
},
},
want: &mythicjwt.CustomClaims{
UserID: 123,
AuthMethod: mythicjwt.AUTH_METHOD_USER,
StandardClaims: jwt.StandardClaims{
IssuedAt: time.Now().Unix(),
ExpiresAt: time.Now().Add(mythicjwt.JWTTimespan).UTC().Unix(),
},
},
wantErr: false,
},
{
name: "Empty apitoken",
args: args{
header: map[string][]string{
"Apitoken": {""},
},
},
want: nil,
wantErr: true,
},
{
name: "No auth",
args: args{
@@ -166,7 +137,6 @@ func TestGetClaims(t *testing.T) {
name: "Both token bad",
args: args{
header: map[string][]string{
"Apitoken": {"badtoken"},
"Authorization": {"Bearer bad"},
},
},
@@ -178,7 +148,8 @@ func TestGetClaims(t *testing.T) {
for _, tt := range tests {
tt := tt // shadowing
t.Run(tt.name, func(t *testing.T) {
// t.Parallel()
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
req := &http.Request{
Header: make(http.Header),
}
+40 -2
View File
@@ -4,7 +4,9 @@ import (
"fmt"
"net"
"net/http"
"slices"
"github.com/its-a-feature/Mythic/authentication/mythicjwt"
"github.com/its-a-feature/Mythic/rabbitmq"
"github.com/gin-gonic/gin"
@@ -44,12 +46,12 @@ func IPBlockMiddleware() gin.HandlerFunc {
return
}
}
logging.LogError(nil, "Client IP not in allowed IP blocks", "client_ip", ipAddr)
go rabbitmq.SendAllOperationsMessage(fmt.Sprintf("Client IP, %s, not in allowed IP blocks: %v", ipAddr.String(), utils.MythicConfig.AllowedIPBlocks),
0, ipAddr.String(), database.MESSAGE_LEVEL_AUTH, true)
c.JSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"})
c.Abort()
return
}
}
@@ -60,12 +62,14 @@ func RBACMiddleware(allowedRoles []string) gin.HandlerFunc {
logging.LogError(err, "Failed to get claims for RBACMiddleware")
c.JSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"})
c.Abort()
return
}
operatorOperation, err := database.GetUserCurrentOperation(customClaims.UserID)
if err != nil {
logging.LogError(err, "Failed to get user current operation")
c.JSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"})
c.Abort()
return
}
if operatorOperation.CurrentOperator.Admin || utils.SliceContains(allowedRoles, operatorOperation.ViewMode) {
c.Set("operatorOperation", operatorOperation)
@@ -76,7 +80,7 @@ func RBACMiddleware(allowedRoles []string) gin.HandlerFunc {
logging.LogError(nil, "Unauthorized view mode for operation")
c.JSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"})
c.Abort()
return
}
}
@@ -100,3 +104,37 @@ func RBACMiddlewareOperationAdmin() gin.HandlerFunc {
database.OPERATOR_OPERATION_VIEW_MODE_LEAD,
})
}
func scopeIncludes(scopes []string, required string) bool {
return slices.Contains(scopes, required)
}
func DirectFileScopeMiddleware(requiredScope string) gin.HandlerFunc {
return func(c *gin.Context) {
claims, err := GetClaims(c)
if err != nil {
logging.LogError(err, "Failed to get claims for DirectFileScopeMiddleware")
c.JSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"})
c.Abort()
return
}
// Backward compatibility: existing normal JWT/APIToken flows without scopes continue to work.
if len(claims.Scopes) == 0 {
c.Next()
return
}
if !scopeIncludes(claims.Scopes, requiredScope) {
c.JSON(http.StatusForbidden, gin.H{"message": "Missing Proper Scope"})
c.Abort()
return
}
if claims.AuthMethod == mythicjwt.AUTH_METHOD_SCOPED &&
claims.FileUUID != "" &&
claims.FileUUID != c.Param("file_uuid") {
c.JSON(http.StatusForbidden, gin.H{"message": "Forbidden"})
c.Abort()
return
}
c.Next()
}
}
@@ -0,0 +1,26 @@
package mythicjwt
import (
"crypto/sha256"
"encoding/hex"
"fmt"
)
const (
APITokenValueHashPrefix = "sha256:"
APITokenValuePrefix = "mtk_"
)
func HashAPITokenValue(token string) string {
sum := sha256.Sum256([]byte(token))
return APITokenValueHashPrefix + hex.EncodeToString(sum[:])
}
func GenerateOpaqueAPIToken() (string, string, error) {
randomValue, err := generateRandomPassword(64)
if err != nil {
return "", "", err
}
plainValue := fmt.Sprintf("%s%s", APITokenValuePrefix, randomValue)
return plainValue, HashAPITokenValue(plainValue), nil
}
@@ -17,11 +17,13 @@ import (
type CustomClaims struct {
jwt.StandardClaims
UserID int `json:"user_id"`
AuthMethod string `json:"auth"`
EventStepInstanceID int `json:"eventstepinstance_id"`
APITokensID int `json:"apitokens_id"`
OperationID int `json:"operation_id"`
UserID int `json:"user_id"`
AuthMethod string `json:"auth"`
EventStepInstanceID int `json:"eventstepinstance_id"`
APITokensID int `json:"apitokens_id"`
OperationID int `json:"operation_id"`
Scopes []string `json:"scopes,omitempty"`
FileUUID string `json:"file_uuid,omitempty"`
}
var (
@@ -33,6 +35,9 @@ var (
AUTH_METHOD_EVENT = "event"
AUTH_METHOD_TASK = "task"
AUTH_METHOD_GRAPHQL_SPECTATOR = "graphql_spectator"
AUTH_METHOD_SCOPED = "scoped"
SCOPE_FILE_DIRECT_UPLOAD = "file.direct.upload"
SCOPE_FILE_DIRECT_DOWNLOAD = "file.direct.download"
ErrFailedToFindRefreshToken = errors.New("Failed to find refresh token for specified access token")
ErrRefreshTokenMissmatch = errors.New("Refresh token doesn't match for the given access token")
ErrUnexpectedSigningMethod = errors.New("Unexpected signing method")
@@ -96,18 +101,20 @@ func RefreshJWT(access_token string, refresh_token string) (string, string, int,
}
func GenerateJWT(user databaseStructs.Operator, authMethod string, eventStepInstanceID int, APITokensID int) (string, string, int, error) {
jwtStandardClaims := jwt.StandardClaims{
IssuedAt: time.Now().UTC().Unix(),
ExpiresAt: time.Now().Add(JWTTimespan).UTC().Unix(),
}
claims := CustomClaims{
jwt.StandardClaims{
IssuedAt: time.Now().UTC().Unix(),
ExpiresAt: time.Now().Add(JWTTimespan).UTC().Unix(),
},
jwtStandardClaims,
user.ID,
authMethod,
eventStepInstanceID,
APITokensID,
int(user.CurrentOperationID.Int64),
nil,
"",
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
access_token, err := token.SignedString(utils.MythicConfig.JWTSecret)
if err != nil {
@@ -127,3 +134,27 @@ func GenerateJWT(user databaseStructs.Operator, authMethod string, eventStepInst
}
return access_token, "", user.ID, nil
}
func GenerateScopedJWT(user databaseStructs.Operator, scopes []string, fileUUID string, ttl time.Duration) (string, int, error) {
jwtStandardClaims := jwt.StandardClaims{
IssuedAt: time.Now().UTC().Unix(),
ExpiresAt: time.Now().Add(ttl).UTC().Unix(),
}
claims := CustomClaims{
jwtStandardClaims,
user.ID,
AUTH_METHOD_SCOPED,
0,
0,
int(user.CurrentOperationID.Int64),
scopes,
fileUUID,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
access_token, err := token.SignedString(utils.MythicConfig.JWTSecret)
if err != nil {
logging.LogError(err, "Failed to generate scoped JWT")
return "", 0, err
}
return access_token, user.ID, nil
}
+4
View File
@@ -2,6 +2,7 @@ package database
import (
"database/sql"
"errors"
"fmt"
mythicCrypto "github.com/its-a-feature/Mythic/crypto"
@@ -71,6 +72,9 @@ func GetUserCurrentOperation(userID int) (*databaseStructs.Operatoroperation, er
}
func GetUserFromID(userID int) (*databaseStructs.Operator, error) {
if DB == nil {
return nil, errors.New("database connection not initialized")
}
operator := databaseStructs.Operator{}
err := DB.Get(&operator, `SELECT
username, id, "admin", last_login, failed_login_count, salt, "password", secrets, preferences,
@@ -134,7 +134,6 @@ func CreateGraphQLSpectatorAPITokenAndSendOnStartMessage(containerName string) {
TokenValue: "",
Active: true,
}
operatorData := databaseStructs.Operator{}
operatorOperationData := []databaseStructs.Operatoroperation{}
onStartMessage := ContainerOnStartMessage{
ContainerName: containerName,
@@ -163,9 +162,6 @@ func CreateGraphQLSpectatorAPITokenAndSendOnStartMessage(containerName string) {
// current operator is a bot, not deleted, and current operation is this operation
apiToken.OperatorID = operatorOperationData[i].CurrentOperator.ID
apiToken.CreatedBy = operatorOperationData[i].CurrentOperator.ID
operatorData.ID = operatorOperationData[i].CurrentOperator.ID
operatorData.CurrentOperationID.Valid = true
operatorData.CurrentOperationID.Int64 = int64(operation.ID)
onStartMessage.OperationID = operatorOperationData[i].CurrentOperation.ID
onStartMessage.OperationName = operatorOperationData[i].CurrentOperation.Name
apiToken.Name = fmt.Sprintf("\"%s\"'s OnStart API Call for \"%s\". Deletes after 5min",
@@ -193,18 +189,18 @@ func CreateGraphQLSpectatorAPITokenAndSendOnStartMessage(containerName string) {
logging.LogError(err, "failed to create new apitoken")
continue
}
accessToken, _, _, err := mythicjwt.GenerateJWT(operatorData, apiToken.TokenType, 0, apiToken.ID)
plainAPITokenValue, storedAPITokenValue, err := mythicjwt.GenerateOpaqueAPIToken()
if err != nil {
logging.LogError(err, "failed to generate new JWT")
logging.LogError(err, "failed to generate new API token")
continue
}
apiToken.TokenValue = accessToken
apiToken.TokenValue = storedAPITokenValue
_, err = database.DB.Exec(`UPDATE apitokens SET token_value=$1 WHERE id=$2`, apiToken.TokenValue, apiToken.ID)
if err != nil {
logging.LogError(err, "Failed to update apitoken with value")
continue
}
onStartMessage.APIToken = apiToken.TokenValue
onStartMessage.APIToken = plainAPITokenValue
go updateAPITokenAfter5Minutes(apiToken.ID)
err = RabbitMQConnection.SendContainerOnStart(onStartMessage)
if err != nil {
+2
View File
@@ -244,6 +244,8 @@ const (
MYTHIC_RPC_C2_UPDATE_STATUS = "mythic_rpc_c2_update_status"
//
MYTHIC_RPC_APITOKEN_CREATE = "mythic_rpc_apitoken_create"
//
MYTHIC_RPC_DIRECT_FILE_TOKEN_CREATE = "mythic_rpc_direct_file_token_create"
// Agent Message
MYTHIC_RPC_HANDLE_AGENT_JSON = "mythic_rpc_handle_agent_message_json"
// CustomBrowser
+3 -4
View File
@@ -1047,19 +1047,18 @@ func startEventStepInstance(eventStepInstanceID int) error {
logging.LogError(err, "Failed to get new apitoken")
continue
}
accessToken, _, _, err := mythicjwt.GenerateJWT(databaseStructs.Operator{ID: eventStepInstance.OperatorID},
mythicjwt.AUTH_METHOD_EVENT, eventStepInstance.ID, apiToken.ID)
plainAPITokenValue, storedAPITokenValue, err := mythicjwt.GenerateOpaqueAPIToken()
if err != nil {
logging.LogError(err, "Failed to generate access token")
continue
}
apiToken.TokenValue = accessToken
apiToken.TokenValue = storedAPITokenValue
_, err = database.DB.Exec(`UPDATE apitokens SET token_value=$1 WHERE id=$2`, apiToken.TokenValue, apiToken.ID)
if err != nil {
logging.LogError(err, "Failed to update apitoken")
continue
}
inputs[key] = apiToken.TokenValue
inputs[key] = plainAPITokenValue
}
}
for i := 0; i < len(allEventSteps); i++ {
@@ -42,7 +42,6 @@ func MythicRPCAPITokenCreate(input MythicRPCAPITokenCreateMessage) MythicRPCAPIT
TokenValue: "",
Active: true,
}
operatorData := databaseStructs.Operator{}
if input.AgentTaskID != nil {
task := databaseStructs.Task{AgentTaskID: *input.AgentTaskID}
err := database.DB.Get(&task, `SELECT id, operator_id, operation_id, display_id FROM task WHERE agent_task_id=$1`, task.AgentTaskID)
@@ -55,7 +54,6 @@ func MythicRPCAPITokenCreate(input MythicRPCAPITokenCreateMessage) MythicRPCAPIT
apiToken.CreatedBy = task.OperatorID
apiToken.TaskID.Valid = true
apiToken.TaskID.Int64 = int64(task.ID)
operatorData.ID = task.OperatorID
apiToken.Name = fmt.Sprintf("Generated Task API Token via MythicRPC for Task %d", task.DisplayID)
} else if input.PayloadUUID != nil {
payload := databaseStructs.Payload{UuID: *input.PayloadUUID}
@@ -67,7 +65,6 @@ func MythicRPCAPITokenCreate(input MythicRPCAPITokenCreateMessage) MythicRPCAPIT
apiToken.TokenType = mythicjwt.AUTH_METHOD_TASK
apiToken.OperatorID = payload.OperatorID
apiToken.CreatedBy = payload.OperatorID
operatorData.ID = payload.OperatorID
apiToken.PayloadID.Valid = true
apiToken.PayloadID.Int64 = int64(payload.ID)
apiToken.Name = fmt.Sprintf("Generated Payload API Token via MythicRPC for Payload %s", payload.UuID)
@@ -97,9 +94,6 @@ func MythicRPCAPITokenCreate(input MythicRPCAPITokenCreateMessage) MythicRPCAPIT
if !operatorOperationData[i].CurrentOperator.Deleted && operatorOperationData[i].CurrentOperator.Active {
apiToken.OperatorID = operatorOperationData[i].CurrentOperator.ID
apiToken.CreatedBy = operatorOperationData[i].CurrentOperator.ID
operatorData.ID = operatorOperationData[i].CurrentOperator.ID
operatorData.CurrentOperationID.Valid = true
operatorData.CurrentOperationID.Int64 = int64(callback.OperationID)
apiToken.Name = fmt.Sprintf("Generated Callback API Token via MythicRPC for Callback %d", callback.DisplayID)
}
}
@@ -129,12 +123,12 @@ func MythicRPCAPITokenCreate(input MythicRPCAPITokenCreateMessage) MythicRPCAPIT
response.Error = err.Error()
return response
}
accessToken, _, _, err := mythicjwt.GenerateJWT(operatorData, apiToken.TokenType, 0, apiToken.ID)
accessToken, storedAPITokenValue, err := mythicjwt.GenerateOpaqueAPIToken()
if err != nil {
response.Error = err.Error()
return response
}
apiToken.TokenValue = accessToken
apiToken.TokenValue = storedAPITokenValue
_, err = database.DB.Exec(`UPDATE apitokens SET token_value=$1 WHERE id=$2`, apiToken.TokenValue, apiToken.ID)
if err != nil {
response.Error = err.Error()
@@ -0,0 +1,143 @@
package rabbitmq
import (
"database/sql"
"encoding/json"
"fmt"
"time"
"github.com/its-a-feature/Mythic/authentication/mythicjwt"
"github.com/its-a-feature/Mythic/database"
databaseStructs "github.com/its-a-feature/Mythic/database/structs"
"github.com/its-a-feature/Mythic/logging"
amqp "github.com/rabbitmq/amqp091-go"
)
const directFileScopedTokenTTL = 30 * time.Second
type MythicRPCDirectFileTokenCreateMessage struct {
AgentTaskID *string `json:"agent_task_id"`
AgentCallbackID *string `json:"agent_callback_id"`
PayloadUUID *string `json:"payload_uuid"`
FileUUID string `json:"file_uuid"`
Action string `json:"action"` // upload, download, both
}
type MythicRPCDirectFileTokenCreateMessageResponse struct {
Success bool `json:"success"`
Error string `json:"error"`
Token string `json:"token"`
}
func init() {
RabbitMQConnection.AddRPCQueue(RPCQueueStruct{
Exchange: MYTHIC_EXCHANGE,
Queue: MYTHIC_RPC_DIRECT_FILE_TOKEN_CREATE,
RoutingKey: MYTHIC_RPC_DIRECT_FILE_TOKEN_CREATE,
Handler: processMythicRPCDirectFileTokenCreate,
})
}
func scopesForDirectFileAction(action string) ([]string, error) {
switch action {
case "upload":
return []string{mythicjwt.SCOPE_FILE_DIRECT_UPLOAD}, nil
case "download":
return []string{mythicjwt.SCOPE_FILE_DIRECT_DOWNLOAD}, nil
case "both", "":
return []string{mythicjwt.SCOPE_FILE_DIRECT_UPLOAD, mythicjwt.SCOPE_FILE_DIRECT_DOWNLOAD}, nil
default:
return nil, fmt.Errorf("invalid action %q (expected upload, download, or both)", action)
}
}
func resolveScopedTokenOperatorAndOperation(input MythicRPCDirectFileTokenCreateMessage) (int, int, error) {
if input.AgentTaskID != nil {
task := databaseStructs.Task{}
if err := database.DB.Get(&task, `SELECT operator_id, operation_id FROM task WHERE agent_task_id=$1`,
*input.AgentTaskID); err != nil {
return 0, 0, err
}
return task.OperatorID, task.OperationID, nil
}
if input.PayloadUUID != nil {
payload := databaseStructs.Payload{}
if err := database.DB.Get(&payload, `SELECT operator_id, operation_id FROM payload WHERE uuid=$1`,
*input.PayloadUUID); err != nil {
return 0, 0, err
}
return payload.OperatorID, payload.OperationID, nil
}
if input.AgentCallbackID != nil {
callback := databaseStructs.Callback{}
if err := database.DB.Get(&callback, `SELECT operation_id FROM callback WHERE agent_callback_id=$1`,
*input.AgentCallbackID); err != nil {
return 0, 0, err
}
operatorOperationData := []databaseStructs.Operatoroperation{}
if err := database.DB.Select(&operatorOperationData, `SELECT
operator.account_type "operator.account_type",
operator.id "operator.id",
operator.deleted "operator.deleted",
operator.active "operator.active"
FROM operatoroperation
JOIN operator ON operatoroperation.operator_id = operator.id
WHERE operatoroperation.operation_id=$1`, callback.OperationID); err != nil {
return 0, 0, err
}
for i := range operatorOperationData {
if operatorOperationData[i].CurrentOperator.AccountType == databaseStructs.AccountTypeBot &&
!operatorOperationData[i].CurrentOperator.Deleted &&
operatorOperationData[i].CurrentOperator.Active {
return operatorOperationData[i].CurrentOperator.ID, callback.OperationID, nil
}
}
return 0, 0, fmt.Errorf("need an active non-deleted bot account assigned to operation %d", callback.OperationID)
}
return 0, 0, fmt.Errorf("no operator/task/callback/payload information provided")
}
func MythicRPCDirectFileTokenCreate(input MythicRPCDirectFileTokenCreateMessage) MythicRPCDirectFileTokenCreateMessageResponse {
response := MythicRPCDirectFileTokenCreateMessageResponse{Success: false}
if input.FileUUID == "" {
response.Error = "file_uuid is required"
return response
}
scopes, err := scopesForDirectFileAction(input.Action)
if err != nil {
response.Error = err.Error()
return response
}
operatorID, operationID, err := resolveScopedTokenOperatorAndOperation(input)
if err != nil {
response.Error = err.Error()
return response
}
user := databaseStructs.Operator{
ID: operatorID,
CurrentOperationID: sql.NullInt64{
Valid: true,
Int64: int64(operationID),
},
}
token, _, err := mythicjwt.GenerateScopedJWT(user, scopes, input.FileUUID, directFileScopedTokenTTL)
if err != nil {
response.Error = err.Error()
return response
}
response.Success = true
response.Token = token
return response
}
func processMythicRPCDirectFileTokenCreate(msg amqp.Delivery) interface{} {
incomingMessage := MythicRPCDirectFileTokenCreateMessage{}
responseMsg := MythicRPCDirectFileTokenCreateMessageResponse{Success: false}
if err := json.Unmarshal(msg.Body, &incomingMessage); err != nil {
logging.LogError(err, "Failed to unmarshal JSON into struct")
responseMsg.Error = err.Error()
} else {
return MythicRPCDirectFileTokenCreate(incomingMessage)
}
return responseMsg
}
@@ -576,7 +576,7 @@ func associateCommandsWithPayload(databasePayload databaseStructs.Payload, comma
if utils.SliceContains(deniedCommandNames, dependency) {
// uh oh, we included a command that depends on a rejected command
return nil, errors.New(fmt.Sprintf("%s depends on %s, but %s isn't allowed with the currently selected parameters",
command.Cmd, dependency))
command.Cmd, dependency, dependency))
}
// the dependency we have isn't included, but isn't explicitly denied, so just add it
finalCommandNames = append(finalCommandNames, dependency)
@@ -3,7 +3,7 @@ package rabbitmq
import (
"database/sql"
"encoding/base64"
"encoding/json/v2"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
@@ -115,10 +115,14 @@ func GenerateAPITokenWebhook(c *gin.Context) {
Name: defaultName,
CreatedBy: createdBy,
}
if authType == mythicjwt.AUTH_METHOD_EVENT {
apiToken.EventStepInstanceID.Valid = true
apiToken.EventStepInstanceID.Int64 = int64(claims.EventStepInstanceID)
}
statement, err := database.DB.PrepareNamed(`INSERT INTO apitokens
(token_value, operator_id, token_type, active, "name", created_by)
(token_value, operator_id, token_type, active, "name", created_by, eventstepinstance_id)
VALUES
(:token_value, :operator_id, :token_type, :active, :name, :created_by)
(:token_value, :operator_id, :token_type, :active, :name, :created_by, :eventstepinstance_id)
RETURNING id`)
if err != nil {
c.JSON(http.StatusOK, GenerateAPITokenResponse{
@@ -135,9 +139,7 @@ func GenerateAPITokenWebhook(c *gin.Context) {
})
return
}
access_token, _, _, err := mythicjwt.GenerateJWT(databaseStructs.Operator{
ID: userID,
}, authType, claims.EventStepInstanceID, apiToken.ID)
accessToken, storedAPITokenValue, err := mythicjwt.GenerateOpaqueAPIToken()
if err != nil {
c.JSON(http.StatusOK, GenerateAPITokenResponse{
Status: "error",
@@ -145,7 +147,7 @@ func GenerateAPITokenWebhook(c *gin.Context) {
})
return
}
apiToken.TokenValue = access_token
apiToken.TokenValue = storedAPITokenValue
_, err = database.DB.Exec(`UPDATE apitokens SET token_value=$1 WHERE id=$2`, apiToken.TokenValue, apiToken.ID)
if err != nil {
c.JSON(http.StatusOK, GenerateAPITokenResponse{
@@ -156,7 +158,7 @@ func GenerateAPITokenWebhook(c *gin.Context) {
}
c.JSON(http.StatusOK, GenerateAPITokenResponse{
Status: "success",
TokenValue: access_token,
TokenValue: accessToken,
OperatorID: userID,
ID: apiToken.ID,
Name: apiToken.Name,
+34 -31
View File
@@ -5,6 +5,7 @@ import (
"net/http"
"time"
"github.com/its-a-feature/Mythic/authentication/mythicjwt"
"github.com/its-a-feature/Mythic/database"
databaseStructs "github.com/its-a-feature/Mythic/database/structs"
"github.com/its-a-feature/Mythic/rabbitmq"
@@ -107,7 +108,7 @@ func InitializeGinLogger() gin.HandlerFunc {
source = "scripting"
}
graphQLOperationName := c.GetString("GraphQLName")
if tokenStruct, ok := c.Get("apitoken_logging_struct"); ok {
if tokenStruct, ok := c.Get(authentication.ContextKeyAPIToken); ok {
if param.Path == "/graphql/webhook" {
if graphQLOperationName == "" {
graphQLOperationName = " with a subscription"
@@ -130,8 +131,8 @@ func InitializeGinLogger() gin.HandlerFunc {
"latency", param.Latency.String(),
"responseSize", param.BodySize,
"source", source,
"user_id", c.GetInt("user_id"),
"username", c.GetString("username"),
"user_id", c.GetInt(authentication.ContextKeyUserID),
"username", c.GetString(authentication.ContextKeyUsername),
"file_id", c.GetString("file_id"),
"graphql_name", c.GetString("GraphQLName"),
"error", param.ErrorMessage)
@@ -142,15 +143,12 @@ func InitializeGinLogger() gin.HandlerFunc {
func setRoutes(r *gin.Engine) {
// the agent message route is allowed to come from anywhere since it'll come through random c2
r.POST("/api/v1.4/agent_message", webcontroller.AgentMessageWebhook)
r.GET("/api/v1.4/agent_message", webcontroller.AgentMessageGetWebhook)
r.POST("/agent_message", webcontroller.AgentMessageWebhook)
r.GET("/agent_message", webcontroller.AgentMessageGetWebhook)
// healthcheck endpoint
r.Use(authentication.IPBlockMiddleware())
{
r.GET("/health", webcontroller.HealthCheckSimple)
r.GET("/healthDetailed", webcontroller.HealthCheckDetailed)
// login page
r.POST("/auth", webcontroller.Login)
r.POST("/invite", webcontroller.UseInviteLink)
@@ -163,43 +161,48 @@ func setRoutes(r *gin.Engine) {
r.GET("/auth_redirect/:containerName/:IDPName", webcontroller.GetAuthContainerRedirect)
r.GET("/auth_acs/:containerName/:IDPName", webcontroller.ProcessIDPResponse)
r.POST("/auth_acs/:containerName/:IDPName", webcontroller.ProcessIDPResponse)
// unauthenticated file download based on file UUID
// this is for payload hosting and payload containers to fetch files via web
r.GET("/direct/download/:file_uuid", webcontroller.FileDirectDownloadWebhook)
// unauthenticated file upload based on file UUID
// this is for payload containers to upload files that are too big for rabbitmq
r.POST("/direct/upload/:file_uuid", webcontroller.FileDirectUploadWebhook)
// create a protected group that requires valid auth with a JWT to access
protected := r.Group("/")
protected.Use(authentication.JwtAuthMiddleware())
{
// EVERYBODY that can authenticate can do the following
r.GET("/healthDetailed", webcontroller.HealthCheckDetailed)
// hasura's graphql endpoint to get updated claims for access control
//protected.GET("/graphql/webhook", webcontroller.GetHasuraClaims)
protected.POST("/graphql/webhook", webcontroller.GetHasuraClaims)
// user
protected.GET("/me", webcontroller.GetMe) // controller.login
protected.POST("/api/v1.4/me_webhook", webcontroller.GetMeWebhook) // controller.login
protected.POST("/api/v1.4/generate_apitoken_webhook", webcontroller.GenerateAPITokenWebhook)
protected.POST("/api/v1.4/delete_apitoken_webhook", webcontroller.DeleteAPITokenWebhook)
protected.POST("/api/v1.4/update_current_operation_webhook", webcontroller.UpdateCurrentOperationWebhook)
protected.POST("/api/v1.4/update_operator_password_webhook", webcontroller.UpdateOperatorPasswordWebhook)
protected.POST("/api/v1.4/create_operator", webcontroller.CreateOperatorWebhook)
protected.POST("/api/v1.4/operator_get_secrets_webhook", webcontroller.GetSecretsWebhook)
protected.POST("/api/v1.4/operator_update_secrets_webhook", webcontroller.UpdateSecretsWebhook)
protected.POST("/api/v1.4/operator_get_preferences_webhook", webcontroller.GetPreferencesWebhook)
protected.POST("/api/v1.4/operator_update_preferences_webhook", webcontroller.UpdatePreferencesWebhook)
protected.GET("/me", webcontroller.GetMe) // controller.login
protected.POST("/me_webhook", webcontroller.GetMeWebhook) // controller.login
protected.POST("/generate_apitoken_webhook", webcontroller.GenerateAPITokenWebhook)
protected.POST("/delete_apitoken_webhook", webcontroller.DeleteAPITokenWebhook)
protected.POST("/update_current_operation_webhook", webcontroller.UpdateCurrentOperationWebhook)
protected.POST("/update_operator_password_webhook", webcontroller.UpdateOperatorPasswordWebhook)
protected.POST("/create_operator", webcontroller.CreateOperatorWebhook)
protected.POST("/operator_get_secrets_webhook", webcontroller.GetSecretsWebhook)
protected.POST("/operator_update_secrets_webhook", webcontroller.UpdateSecretsWebhook)
protected.POST("/operator_get_preferences_webhook", webcontroller.GetPreferencesWebhook)
protected.POST("/operator_update_preferences_webhook", webcontroller.UpdatePreferencesWebhook)
// operation
protected.POST("/api/v1.4/create_operation_webhook", webcontroller.CreateOperationWebhook)
protected.POST("/create_operation_webhook", webcontroller.CreateOperationWebhook)
// global config
protected.POST("/api/v1.4/get_global_settings_webhook", webcontroller.GetGlobalSettingWebhook)
protected.POST("/get_global_settings_webhook", webcontroller.GetGlobalSettingWebhook)
// a refresh post will contain the access_token and refresh_token
protected.POST("/refresh", webcontroller.RefreshJWT)
protected.Static("/static", "./static")
protected.GET("direct/view/:file_uuid", webcontroller.FileDirectViewWebhook)
protected.GET("/direct/view/:file_uuid",
authentication.DirectFileScopeMiddleware(mythicjwt.SCOPE_FILE_DIRECT_DOWNLOAD),
webcontroller.FileDirectViewWebhook)
// authenticated direct file routes.
// Supports normal JWT/APIToken auth and scoped short-lived direct-file tokens.
r.GET("/direct/download/:file_uuid",
authentication.DirectFileScopeMiddleware(mythicjwt.SCOPE_FILE_DIRECT_DOWNLOAD),
webcontroller.FileDirectDownloadWebhook)
r.POST("/direct/upload/:file_uuid",
authentication.DirectFileScopeMiddleware(mythicjwt.SCOPE_FILE_DIRECT_UPLOAD),
webcontroller.FileDirectUploadWebhook)
// following require you to have an operation set
allOperationMembers := protected.Group("/api/v1.4/")
allOperationMembers := protected.Group("/")
allOperationMembers.Use(authentication.RBACMiddlewareAll())
{
// generic all installed services
@@ -216,9 +219,9 @@ func setRoutes(r *gin.Engine) {
// file
allOperationMembers.POST("download_bulk_webhook", webcontroller.DownloadBulkFilesWebhook)
allOperationMembers.POST("preview_file_webhook", webcontroller.PreviewFileWebhook)
allOperationMembers.GET("files/screencaptures/:file_uuid", webcontroller.DownloadFileAuthWebhook)
allOperationMembers.GET("screencaptures/:file_uuid", webcontroller.DownloadFileAuthWebhook)
}
noSpectators := protected.Group("/api/v1.4/")
noSpectators := protected.Group("/")
noSpectators.Use(authentication.RBACMiddlewareNoSpectators())
{
// everybody EXCEPT SPECTATORS can do these actions
@@ -302,7 +305,7 @@ func setRoutes(r *gin.Engine) {
// custom browsers
noSpectators.POST("custombrowser_export_function_webhook", webcontroller.CustomBrowserExportFunctionWebhook)
}
operationAdminsOnly := protected.Group("/api/v1.4/")
operationAdminsOnly := protected.Group("/")
operationAdminsOnly.Use(authentication.RBACMiddlewareOperationAdmin())
{
// Only OPERATION_ADMIN and MYTHIC_ADMIN can do these routes