Add /api/v3/iam/user endpoints

This commit is contained in:
Ingo Oppermann
2023-03-07 16:31:58 +01:00
parent b006840002
commit 8755117e92
29 changed files with 1608 additions and 442 deletions

View File

@@ -444,38 +444,38 @@ func (a *api) start() error {
// Create default policies for anonymous users in order to mimic // Create default policies for anonymous users in order to mimic
// the behaviour before IAM // the behaviour before IAM
iam.RemovePolicy("$anon", "$none", "", "") iam.RemovePolicy("$anon", "$none", "", nil)
iam.RemovePolicy("$localhost", "$none", "", "") iam.RemovePolicy("$localhost", "$none", "", nil)
iam.AddPolicy("$anon", "$none", "fs:/**", "GET|HEAD|OPTIONS") iam.AddPolicy("$anon", "$none", "fs:/**", []string{"GET", "HEAD", "OPTIONS"})
iam.AddPolicy("$anon", "$none", "api:/api", "GET|HEAD|OPTIONS") iam.AddPolicy("$anon", "$none", "api:/api", []string{"GET", "HEAD", "OPTIONS"})
iam.AddPolicy("$anon", "$none", "api:/api/v3/widget/process/**", "GET|HEAD|OPTIONS") iam.AddPolicy("$anon", "$none", "api:/api/v3/widget/process/**", []string{"GET", "HEAD", "OPTIONS"})
iam.AddPolicy("$localhost", "$none", "api:/api", "GET|HEAD|OPTIONS") iam.AddPolicy("$localhost", "$none", "api:/api", []string{"GET", "HEAD", "OPTIONS"})
iam.AddPolicy("$localhost", "$none", "api:/api/v3/widget/process/**", "GET|HEAD|OPTIONS") iam.AddPolicy("$localhost", "$none", "api:/api/v3/widget/process/**", []string{"GET", "HEAD", "OPTIONS"})
if !cfg.API.Auth.Enable { if !cfg.API.Auth.Enable {
iam.AddPolicy("$anon", "$none", "api:/api/**", "ANY") iam.AddPolicy("$anon", "$none", "api:/api/**", []string{"ANY"})
iam.AddPolicy("$anon", "$none", "process:*", "ANY") iam.AddPolicy("$anon", "$none", "process:*", []string{"ANY"})
iam.AddPolicy("$localhost", "$none", "api:/api/**", "ANY") iam.AddPolicy("$localhost", "$none", "api:/api/**", []string{"ANY"})
iam.AddPolicy("$localhost", "$none", "process:*", "ANY") iam.AddPolicy("$localhost", "$none", "process:*", []string{"ANY"})
} else { } else {
if cfg.API.Auth.DisableLocalhost { if cfg.API.Auth.DisableLocalhost {
iam.AddPolicy("$localhost", "$none", "api:/api/**", "ANY") iam.AddPolicy("$localhost", "$none", "api:/api/**", []string{"ANY"})
iam.AddPolicy("$localhost", "$none", "process:*", "ANY") iam.AddPolicy("$localhost", "$none", "process:*", []string{"ANY"})
} }
} }
if !cfg.Storage.Memory.Auth.Enable { if !cfg.Storage.Memory.Auth.Enable {
iam.AddPolicy("$anon", "$none", "fs:/memfs/**", "ANY") iam.AddPolicy("$anon", "$none", "fs:/memfs/**", []string{"ANY"})
} }
if cfg.RTMP.Enable && len(cfg.RTMP.Token) == 0 { if cfg.RTMP.Enable && len(cfg.RTMP.Token) == 0 {
iam.AddPolicy("$anon", "$none", "rtmp:/**", "ANY") iam.AddPolicy("$anon", "$none", "rtmp:/**", []string{"ANY"})
} }
if cfg.SRT.Enable && len(cfg.SRT.Token) == 0 { if cfg.SRT.Enable && len(cfg.SRT.Token) == 0 {
iam.AddPolicy("$anon", "$none", "srt:**", "ANY") iam.AddPolicy("$anon", "$none", "srt:**", []string{"ANY"})
} }
a.iam = iam a.iam = iam
@@ -672,9 +672,9 @@ func (a *api) start() error {
var identity iam.IdentityVerifier = nil var identity iam.IdentityVerifier = nil
if len(config.Owner) == 0 { if len(config.Owner) == 0 {
identity, _ = a.iam.GetDefaultIdentity() identity, _ = a.iam.GetDefaultVerifier()
} else { } else {
identity, _ = a.iam.GetIdentity(config.Owner) identity, _ = a.iam.GetVerifier(config.Owner)
} }
if identity != nil { if identity != nil {
@@ -698,9 +698,9 @@ func (a *api) start() error {
var identity iam.IdentityVerifier = nil var identity iam.IdentityVerifier = nil
if len(config.Owner) == 0 { if len(config.Owner) == 0 {
identity, _ = a.iam.GetDefaultIdentity() identity, _ = a.iam.GetDefaultVerifier()
} else { } else {
identity, _ = a.iam.GetIdentity(config.Owner) identity, _ = a.iam.GetVerifier(config.Owner)
} }
if identity != nil { if identity != nil {

View File

@@ -1440,7 +1440,7 @@ func probeInput(binary string, config app.Config) app.Probe {
Logger: nil, Logger: nil,
}) })
iam.AddPolicy("$anon", "$none", "process:*", "CREATE|GET|DELETE|PROBE") iam.AddPolicy("$anon", "$none", "process:*", []string{"CREATE", "GET", "DELETE", "PROBE"})
rs, err := restream.New(restream.Config{ rs, err := restream.New(restream.Config{
FFmpeg: ffmpeg, FFmpeg: ffmpeg,

View File

@@ -1,5 +1,4 @@
// Package docs GENERATED BY SWAG; DO NOT EDIT // Code generated by swaggo/swag. DO NOT EDIT
// This file was generated by swaggo/swag
package docs package docs
import "github.com/swaggo/swag" import "github.com/swaggo/swag"
@@ -110,87 +109,6 @@ const docTemplate = `{
} }
} }
}, },
"/api/login": {
"post": {
"security": [
{
"Auth0KeyAuth": []
}
],
"description": "Retrieve valid JWT access and refresh tokens to use for accessing the API. Login either by username/password or Auth0 token",
"produces": [
"application/json"
],
"summary": "Retrieve an access and a refresh token",
"operationId": "jwt-login",
"parameters": [
{
"description": "Login data",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api.Login"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api.JWT"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/api.Error"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/api.Error"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/api.Error"
}
}
}
}
},
"/api/login/refresh": {
"get": {
"security": [
{
"ApiRefreshKeyAuth": []
}
],
"description": "Retrieve a new access token by providing the refresh token",
"produces": [
"application/json"
],
"summary": "Retrieve a new access token",
"operationId": "jwt-refresh",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api.JWTRefresh"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/api.Error"
}
}
}
}
},
"/api/swagger": { "/api/swagger": {
"get": { "get": {
"description": "Swagger UI for this API", "description": "Swagger UI for this API",
@@ -550,6 +468,207 @@ const docTemplate = `{
} }
} }
}, },
"/api/v3/iam/user": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Add a new user",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"v16.?.?"
],
"summary": "Add a new user",
"operationId": "iam-3-add-user",
"parameters": [
{
"description": "User definition",
"name": "config",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api.IAMUser"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api.IAMUser"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/api.Error"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/api.Error"
}
}
}
}
},
"/api/v3/iam/user/{name}": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "List aa user by its name",
"produces": [
"application/json"
],
"tags": [
"v16.?.?"
],
"summary": "List an user by its name",
"operationId": "iam-3-get-user",
"parameters": [
{
"type": "string",
"description": "Username",
"name": "name",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api.IAMUser"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/api.Error"
}
}
}
},
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Replace an existing user.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"v16.?.?"
],
"summary": "Replace an existing user",
"operationId": "iam-3-update-user",
"parameters": [
{
"type": "string",
"description": "Username",
"name": "name",
"in": "path",
"required": true
},
{
"description": "User definition",
"name": "user",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api.IAMUser"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api.IAMUser"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/api.Error"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/api.Error"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/api.Error"
}
}
}
},
"delete": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Delete an user by its name",
"produces": [
"application/json"
],
"tags": [
"v16.?.?"
],
"summary": "Delete an user by its name",
"operationId": "iam-3-delete-user",
"parameters": [
{
"type": "string",
"description": "Username",
"name": "name",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/api.Error"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/api.Error"
}
}
}
}
},
"/api/v3/log": { "/api/v3/log": {
"get": { "get": {
"security": [ "security": [
@@ -2690,22 +2809,115 @@ const docTemplate = `{
} }
} }
}, },
"api.JWT": { "api.IAMAuth0Tenant": {
"type": "object", "type": "object",
"properties": { "properties": {
"access_token": { "audience": {
"type": "string" "type": "string"
}, },
"refresh_token": { "client_id": {
"type": "string"
},
"domain": {
"type": "string" "type": "string"
} }
} }
}, },
"api.JWTRefresh": { "api.IAMPolicy": {
"type": "object", "type": "object",
"properties": { "properties": {
"access_token": { "actions": {
"type": "array",
"items": {
"type": "string"
}
},
"group": {
"type": "string" "type": "string"
},
"resource": {
"type": "string"
}
}
},
"api.IAMUser": {
"type": "object",
"properties": {
"auth": {
"$ref": "#/definitions/api.IAMUserAuth"
},
"name": {
"type": "string"
},
"policies": {
"type": "array",
"items": {
"$ref": "#/definitions/api.IAMPolicy"
}
},
"superuser": {
"type": "boolean"
}
}
},
"api.IAMUserAuth": {
"type": "object",
"properties": {
"api": {
"$ref": "#/definitions/api.IAMUserAuthAPI"
},
"services": {
"$ref": "#/definitions/api.IAMUserAuthServices"
}
}
},
"api.IAMUserAuthAPI": {
"type": "object",
"properties": {
"auth0": {
"$ref": "#/definitions/api.IAMUserAuthAPIAuth0"
},
"userpass": {
"$ref": "#/definitions/api.IAMUserAuthPassword"
}
}
},
"api.IAMUserAuthAPIAuth0": {
"type": "object",
"properties": {
"enable": {
"type": "boolean"
},
"tenant": {
"$ref": "#/definitions/api.IAMAuth0Tenant"
},
"user": {
"type": "string"
}
}
},
"api.IAMUserAuthPassword": {
"type": "object",
"properties": {
"enable": {
"type": "boolean"
},
"password": {
"type": "string"
}
}
},
"api.IAMUserAuthServices": {
"type": "object",
"properties": {
"basic": {
"$ref": "#/definitions/api.IAMUserAuthPassword"
},
"token": {
"type": "array",
"items": {
"type": "string"
}
} }
} }
}, },
@@ -2713,21 +2925,6 @@ const docTemplate = `{
"type": "object", "type": "object",
"additionalProperties": true "additionalProperties": true
}, },
"api.Login": {
"type": "object",
"required": [
"password",
"username"
],
"properties": {
"password": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"api.MetricsDescription": { "api.MetricsDescription": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -3043,6 +3240,9 @@ const docTemplate = `{
"autostart": { "autostart": {
"type": "boolean" "type": "boolean"
}, },
"group": {
"type": "string"
},
"id": { "id": {
"type": "string" "type": "string"
}, },

View File

@@ -102,87 +102,6 @@
} }
} }
}, },
"/api/login": {
"post": {
"security": [
{
"Auth0KeyAuth": []
}
],
"description": "Retrieve valid JWT access and refresh tokens to use for accessing the API. Login either by username/password or Auth0 token",
"produces": [
"application/json"
],
"summary": "Retrieve an access and a refresh token",
"operationId": "jwt-login",
"parameters": [
{
"description": "Login data",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api.Login"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api.JWT"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/api.Error"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/api.Error"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/api.Error"
}
}
}
}
},
"/api/login/refresh": {
"get": {
"security": [
{
"ApiRefreshKeyAuth": []
}
],
"description": "Retrieve a new access token by providing the refresh token",
"produces": [
"application/json"
],
"summary": "Retrieve a new access token",
"operationId": "jwt-refresh",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api.JWTRefresh"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/api.Error"
}
}
}
}
},
"/api/swagger": { "/api/swagger": {
"get": { "get": {
"description": "Swagger UI for this API", "description": "Swagger UI for this API",
@@ -542,6 +461,207 @@
} }
} }
}, },
"/api/v3/iam/user": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Add a new user",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"v16.?.?"
],
"summary": "Add a new user",
"operationId": "iam-3-add-user",
"parameters": [
{
"description": "User definition",
"name": "config",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api.IAMUser"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api.IAMUser"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/api.Error"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/api.Error"
}
}
}
}
},
"/api/v3/iam/user/{name}": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "List aa user by its name",
"produces": [
"application/json"
],
"tags": [
"v16.?.?"
],
"summary": "List an user by its name",
"operationId": "iam-3-get-user",
"parameters": [
{
"type": "string",
"description": "Username",
"name": "name",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api.IAMUser"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/api.Error"
}
}
}
},
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Replace an existing user.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"v16.?.?"
],
"summary": "Replace an existing user",
"operationId": "iam-3-update-user",
"parameters": [
{
"type": "string",
"description": "Username",
"name": "name",
"in": "path",
"required": true
},
{
"description": "User definition",
"name": "user",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api.IAMUser"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api.IAMUser"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/api.Error"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/api.Error"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/api.Error"
}
}
}
},
"delete": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Delete an user by its name",
"produces": [
"application/json"
],
"tags": [
"v16.?.?"
],
"summary": "Delete an user by its name",
"operationId": "iam-3-delete-user",
"parameters": [
{
"type": "string",
"description": "Username",
"name": "name",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/api.Error"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/api.Error"
}
}
}
}
},
"/api/v3/log": { "/api/v3/log": {
"get": { "get": {
"security": [ "security": [
@@ -2682,22 +2802,115 @@
} }
} }
}, },
"api.JWT": { "api.IAMAuth0Tenant": {
"type": "object", "type": "object",
"properties": { "properties": {
"access_token": { "audience": {
"type": "string" "type": "string"
}, },
"refresh_token": { "client_id": {
"type": "string"
},
"domain": {
"type": "string" "type": "string"
} }
} }
}, },
"api.JWTRefresh": { "api.IAMPolicy": {
"type": "object", "type": "object",
"properties": { "properties": {
"access_token": { "actions": {
"type": "array",
"items": {
"type": "string"
}
},
"group": {
"type": "string" "type": "string"
},
"resource": {
"type": "string"
}
}
},
"api.IAMUser": {
"type": "object",
"properties": {
"auth": {
"$ref": "#/definitions/api.IAMUserAuth"
},
"name": {
"type": "string"
},
"policies": {
"type": "array",
"items": {
"$ref": "#/definitions/api.IAMPolicy"
}
},
"superuser": {
"type": "boolean"
}
}
},
"api.IAMUserAuth": {
"type": "object",
"properties": {
"api": {
"$ref": "#/definitions/api.IAMUserAuthAPI"
},
"services": {
"$ref": "#/definitions/api.IAMUserAuthServices"
}
}
},
"api.IAMUserAuthAPI": {
"type": "object",
"properties": {
"auth0": {
"$ref": "#/definitions/api.IAMUserAuthAPIAuth0"
},
"userpass": {
"$ref": "#/definitions/api.IAMUserAuthPassword"
}
}
},
"api.IAMUserAuthAPIAuth0": {
"type": "object",
"properties": {
"enable": {
"type": "boolean"
},
"tenant": {
"$ref": "#/definitions/api.IAMAuth0Tenant"
},
"user": {
"type": "string"
}
}
},
"api.IAMUserAuthPassword": {
"type": "object",
"properties": {
"enable": {
"type": "boolean"
},
"password": {
"type": "string"
}
}
},
"api.IAMUserAuthServices": {
"type": "object",
"properties": {
"basic": {
"$ref": "#/definitions/api.IAMUserAuthPassword"
},
"token": {
"type": "array",
"items": {
"type": "string"
}
} }
} }
}, },
@@ -2705,21 +2918,6 @@
"type": "object", "type": "object",
"additionalProperties": true "additionalProperties": true
}, },
"api.Login": {
"type": "object",
"required": [
"password",
"username"
],
"properties": {
"password": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"api.MetricsDescription": { "api.MetricsDescription": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -3035,6 +3233,9 @@
"autostart": { "autostart": {
"type": "boolean" "type": "boolean"
}, },
"group": {
"type": "string"
},
"id": { "id": {
"type": "string" "type": "string"
}, },

View File

@@ -469,31 +469,81 @@ definitions:
items: {} items: {}
type: array type: array
type: object type: object
api.JWT: api.IAMAuth0Tenant:
properties: properties:
access_token: audience:
type: string type: string
refresh_token: client_id:
type: string
domain:
type: string type: string
type: object type: object
api.JWTRefresh: api.IAMPolicy:
properties: properties:
access_token: actions:
items:
type: string
type: array
group:
type: string type: string
resource:
type: string
type: object
api.IAMUser:
properties:
auth:
$ref: '#/definitions/api.IAMUserAuth'
name:
type: string
policies:
items:
$ref: '#/definitions/api.IAMPolicy'
type: array
superuser:
type: boolean
type: object
api.IAMUserAuth:
properties:
api:
$ref: '#/definitions/api.IAMUserAuthAPI'
services:
$ref: '#/definitions/api.IAMUserAuthServices'
type: object
api.IAMUserAuthAPI:
properties:
auth0:
$ref: '#/definitions/api.IAMUserAuthAPIAuth0'
userpass:
$ref: '#/definitions/api.IAMUserAuthPassword'
type: object
api.IAMUserAuthAPIAuth0:
properties:
enable:
type: boolean
tenant:
$ref: '#/definitions/api.IAMAuth0Tenant'
user:
type: string
type: object
api.IAMUserAuthPassword:
properties:
enable:
type: boolean
password:
type: string
type: object
api.IAMUserAuthServices:
properties:
basic:
$ref: '#/definitions/api.IAMUserAuthPassword'
token:
items:
type: string
type: array
type: object type: object
api.LogEvent: api.LogEvent:
additionalProperties: true additionalProperties: true
type: object type: object
api.Login:
properties:
password:
type: string
username:
type: string
required:
- password
- username
type: object
api.MetricsDescription: api.MetricsDescription:
properties: properties:
description: description:
@@ -706,6 +756,8 @@ definitions:
properties: properties:
autostart: autostart:
type: boolean type: boolean
group:
type: string
id: id:
type: string type: string
input: input:
@@ -1980,58 +2032,6 @@ paths:
security: security:
- ApiKeyAuth: [] - ApiKeyAuth: []
summary: Query the GraphAPI summary: Query the GraphAPI
/api/login:
post:
description: Retrieve valid JWT access and refresh tokens to use for accessing
the API. Login either by username/password or Auth0 token
operationId: jwt-login
parameters:
- description: Login data
in: body
name: data
required: true
schema:
$ref: '#/definitions/api.Login'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/api.JWT'
"400":
description: Bad Request
schema:
$ref: '#/definitions/api.Error'
"403":
description: Forbidden
schema:
$ref: '#/definitions/api.Error'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/api.Error'
security:
- Auth0KeyAuth: []
summary: Retrieve an access and a refresh token
/api/login/refresh:
get:
description: Retrieve a new access token by providing the refresh token
operationId: jwt-refresh
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/api.JWTRefresh'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/api.Error'
security:
- ApiRefreshKeyAuth: []
summary: Retrieve a new access token
/api/swagger: /api/swagger:
get: get:
description: Swagger UI for this API description: Swagger UI for this API
@@ -2266,6 +2266,135 @@ paths:
security: security:
- ApiKeyAuth: [] - ApiKeyAuth: []
summary: Add a file to a filesystem summary: Add a file to a filesystem
/api/v3/iam/user:
post:
consumes:
- application/json
description: Add a new user
operationId: iam-3-add-user
parameters:
- description: User definition
in: body
name: config
required: true
schema:
$ref: '#/definitions/api.IAMUser'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/api.IAMUser'
"400":
description: Bad Request
schema:
$ref: '#/definitions/api.Error'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/api.Error'
security:
- ApiKeyAuth: []
summary: Add a new user
tags:
- v16.?.?
/api/v3/iam/user/{name}:
delete:
description: Delete an user by its name
operationId: iam-3-delete-user
parameters:
- description: Username
in: path
name: name
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
type: string
"404":
description: Not Found
schema:
$ref: '#/definitions/api.Error'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/api.Error'
security:
- ApiKeyAuth: []
summary: Delete an user by its name
tags:
- v16.?.?
get:
description: List aa user by its name
operationId: iam-3-get-user
parameters:
- description: Username
in: path
name: name
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/api.IAMUser'
"404":
description: Not Found
schema:
$ref: '#/definitions/api.Error'
security:
- ApiKeyAuth: []
summary: List an user by its name
tags:
- v16.?.?
put:
consumes:
- application/json
description: Replace an existing user.
operationId: iam-3-update-user
parameters:
- description: Username
in: path
name: name
required: true
type: string
- description: User definition
in: body
name: user
required: true
schema:
$ref: '#/definitions/api.IAMUser'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/api.IAMUser'
"400":
description: Bad Request
schema:
$ref: '#/definitions/api.Error'
"404":
description: Not Found
schema:
$ref: '#/definitions/api.Error'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/api.Error'
security:
- ApiKeyAuth: []
summary: Replace an existing user
tags:
- v16.?.?
/api/v3/log: /api/v3/log:
get: get:
description: Get the last log lines of the Restreamer application description: Get the last log lines of the Restreamer application

125
http/api/iam.go Normal file
View File

@@ -0,0 +1,125 @@
package api
import "github.com/datarhei/core/v16/iam"
type IAMUser struct {
Name string `json:"name"`
Superuser bool `json:"superuser"`
Auth IAMUserAuth `json:"auth"`
Policies []IAMPolicy `json:"policies"`
}
func (u *IAMUser) Marshal(user iam.User, policies []iam.Policy) {
u.Name = user.Name
u.Superuser = user.Superuser
u.Auth = IAMUserAuth{
API: IAMUserAuthAPI{
Userpass: IAMUserAuthPassword{
Enable: user.Auth.API.Userpass.Enable,
Password: user.Auth.API.Userpass.Password,
},
Auth0: IAMUserAuthAPIAuth0{
Enable: false,
User: "",
Tenant: IAMAuth0Tenant{},
},
},
Services: IAMUserAuthServices{
Basic: IAMUserAuthPassword{
Enable: user.Auth.Services.Basic.Enable,
Password: user.Auth.Services.Basic.Password,
},
Token: user.Auth.Services.Token,
},
}
for _, p := range policies {
u.Policies = append(u.Policies, IAMPolicy{
Domain: p.Domain,
Resource: p.Resource,
Actions: p.Actions,
})
}
}
func (u *IAMUser) Unmarshal() (iam.User, []iam.Policy) {
iamuser := iam.User{
Name: u.Name,
Superuser: u.Superuser,
Auth: iam.UserAuth{
API: iam.UserAuthAPI{
Userpass: iam.UserAuthPassword{
Enable: u.Auth.API.Userpass.Enable,
Password: u.Auth.API.Userpass.Password,
},
Auth0: iam.UserAuthAPIAuth0{
Enable: u.Auth.API.Auth0.Enable,
User: u.Auth.API.Auth0.User,
Tenant: iam.Auth0Tenant{
Domain: u.Auth.API.Auth0.Tenant.Domain,
Audience: u.Auth.API.Auth0.Tenant.Audience,
ClientID: u.Auth.API.Auth0.Tenant.ClientID,
},
},
},
Services: iam.UserAuthServices{
Basic: iam.UserAuthPassword{
Enable: u.Auth.Services.Basic.Enable,
Password: u.Auth.Services.Basic.Password,
},
Token: u.Auth.Services.Token,
},
},
}
iampolicies := []iam.Policy{}
for _, p := range u.Policies {
iampolicies = append(iampolicies, iam.Policy{
Name: u.Name,
Domain: p.Domain,
Resource: p.Resource,
Actions: p.Actions,
})
}
return iamuser, iampolicies
}
type IAMUserAuth struct {
API IAMUserAuthAPI `json:"api"`
Services IAMUserAuthServices `json:"services"`
}
type IAMUserAuthAPI struct {
Userpass IAMUserAuthPassword `json:"userpass"`
Auth0 IAMUserAuthAPIAuth0 `json:"auth0"`
}
type IAMUserAuthAPIAuth0 struct {
Enable bool `json:"enable"`
User string `json:"user"`
Tenant IAMAuth0Tenant `json:"tenant"`
}
type IAMUserAuthServices struct {
Basic IAMUserAuthPassword `json:"basic"`
Token []string `json:"token"`
}
type IAMUserAuthPassword struct {
Enable bool `json:"enable"`
Password string `json:"password"`
}
type IAMAuth0Tenant struct {
Domain string `json:"domain"`
Audience string `json:"audience"`
ClientID string `json:"client_id"`
}
type IAMPolicy struct {
Domain string `json:"group"`
Resource string `json:"resource"`
Actions []string `json:"actions"`
}

170
http/handler/api/iam.go Normal file
View File

@@ -0,0 +1,170 @@
package api
import (
"net/http"
"github.com/datarhei/core/v16/http/api"
"github.com/datarhei/core/v16/http/handler/util"
"github.com/datarhei/core/v16/iam"
"github.com/labstack/echo/v4"
)
type IAMHandler struct {
iam iam.IAM
}
func NewIAM(iam iam.IAM) *IAMHandler {
return &IAMHandler{
iam: iam,
}
}
// Add adds a new user
// @Summary Add a new user
// @Description Add a new user
// @Tags v16.?.?
// @ID iam-3-add-user
// @Accept json
// @Produce json
// @Param config body api.IAMUser true "User definition"
// @Success 200 {object} api.IAMUser
// @Failure 400 {object} api.Error
// @Failure 500 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/iam/user [post]
func (h *IAMHandler) AddUser(c echo.Context) error {
//user := util.DefaultContext(c, "user", "")
user := api.IAMUser{}
if err := util.ShouldBindJSON(c, &user); err != nil {
return api.Err(http.StatusBadRequest, "Invalid JSON", "%s", err)
}
iamuser, iampolicies := user.Unmarshal()
err := h.iam.CreateIdentity(iamuser)
if err != nil {
return api.Err(http.StatusBadRequest, "Bad request", "%s", err)
}
for _, p := range iampolicies {
h.iam.AddPolicy(p.Name, p.Domain, p.Resource, p.Actions)
}
err = h.iam.SaveIdentities()
if err != nil {
return api.Err(http.StatusInternalServerError, "Internal server error", "%s", err)
}
return c.JSON(http.StatusOK, user)
}
// Delete deletes the user with the given name
// @Summary Delete an user by its name
// @Description Delete an user by its name
// @Tags v16.?.?
// @ID iam-3-delete-user
// @Produce json
// @Param name path string true "Username"
// @Success 200 {string} string
// @Failure 404 {object} api.Error
// @Failure 500 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/iam/user/{name} [delete]
func (h *IAMHandler) RemoveUser(c echo.Context) error {
name := util.PathParam(c, "name")
err := h.iam.DeleteIdentity(name)
if err != nil {
return api.Err(http.StatusBadRequest, "Bad request", "%s", err)
}
err = h.iam.SaveIdentities()
if err != nil {
return api.Err(http.StatusInternalServerError, "Internal server error", "%s", err)
}
return c.JSON(http.StatusOK, "OK")
}
// Update replaces an existing user
// @Summary Replace an existing user
// @Description Replace an existing user.
// @Tags v16.?.?
// @ID iam-3-update-user
// @Accept json
// @Produce json
// @Param name path string true "Username"
// @Param user body api.IAMUser true "User definition"
// @Success 200 {object} api.IAMUser
// @Failure 400 {object} api.Error
// @Failure 404 {object} api.Error
// @Failure 500 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/iam/user/{name} [put]
func (h *IAMHandler) UpdateUser(c echo.Context) error {
name := util.PathParam(c, "name")
iamuser, err := h.iam.GetIdentity(name)
if err != nil {
return api.Err(http.StatusNotFound, "Not found", "%s", err)
}
iampolicies := h.iam.ListPolicies(name, "", "", nil)
user := api.IAMUser{}
user.Marshal(iamuser, iampolicies)
if err := util.ShouldBindJSON(c, &user); err != nil {
return api.Err(http.StatusBadRequest, "Invalid JSON", "%s", err)
}
iamuser, iampolicies = user.Unmarshal()
err = h.iam.UpdateIdentity(name, iamuser)
if err != nil {
return api.Err(http.StatusBadRequest, "Bad request", "%s", err)
}
h.iam.RemovePolicy(name, "", "", nil)
for _, p := range iampolicies {
h.iam.AddPolicy(p.Name, p.Domain, p.Resource, p.Actions)
}
err = h.iam.SaveIdentities()
if err != nil {
return api.Err(http.StatusInternalServerError, "Internal server error", "%s", err)
}
return c.JSON(http.StatusOK, user)
}
// Get returns the user with the given name
// @Summary List an user by its name
// @Description List aa user by its name
// @Tags v16.?.?
// @ID iam-3-get-user
// @Produce json
// @Param name path string true "Username"
// @Success 200 {object} api.IAMUser
// @Failure 404 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/iam/user/{name} [get]
func (h *IAMHandler) GetUser(c echo.Context) error {
name := util.PathParam(c, "name")
iamuser, err := h.iam.GetIdentity(name)
if err != nil {
return api.Err(http.StatusNotFound, "Not found", "%s", err)
}
iampolicies := h.iam.ListPolicies(name, "", "", nil)
user := api.IAMUser{}
user.Marshal(iamuser, iampolicies)
return c.JSON(http.StatusOK, user)
}

View File

@@ -250,7 +250,7 @@ func (m *iammiddleware) findIdentityFromBasicAuth(c echo.Context) (iam.IdentityV
} }
} }
identity, err := m.iam.GetIdentity(username) identity, err := m.iam.GetVerifier(username)
if err != nil { if err != nil {
m.logger.Debug().WithFields(log.Fields{ m.logger.Debug().WithFields(log.Fields{
"path": c.Request().URL.Path, "path": c.Request().URL.Path,
@@ -314,7 +314,7 @@ func (m *iammiddleware) findIdentityFromJWT(c echo.Context) (iam.IdentityVerifie
} }
} }
identity, err := m.iam.GetIdentity(subject) identity, err := m.iam.GetVerifier(subject)
if err != nil { if err != nil {
m.logger.Debug().WithFields(log.Fields{ m.logger.Debug().WithFields(log.Fields{
"path": c.Request().URL.Path, "path": c.Request().URL.Path,
@@ -343,7 +343,7 @@ func (m *iammiddleware) findIdentityFromUserpass(c echo.Context) (iam.IdentityVe
return nil, nil return nil, nil
} }
identity, err := m.iam.GetIdentity(login.Username) identity, err := m.iam.GetVerifier(login.Username)
if err != nil { if err != nil {
m.logger.Debug().WithFields(log.Fields{ m.logger.Debug().WithFields(log.Fields{
"path": c.Request().URL.Path, "path": c.Request().URL.Path,
@@ -400,7 +400,7 @@ func (m *iammiddleware) findIdentityFromAuth0(c echo.Context) (iam.IdentityVerif
} }
} }
identity, err := m.iam.GetIdentityByAuth0(subject) identity, err := m.iam.GetVerfierFromAuth0(subject)
if err != nil { if err != nil {
m.logger.Debug().WithFields(log.Fields{ m.logger.Debug().WithFields(log.Fields{
"path": c.Request().URL.Path, "path": c.Request().URL.Path,

View File

@@ -82,7 +82,7 @@ func TestBasicAuth(t *testing.T) {
iam, err := getIAM() iam, err := getIAM()
require.NoError(t, err) require.NoError(t, err)
iam.AddPolicy("foobar", "$none", "fs:/**", "ANY") iam.AddPolicy("foobar", "$none", "fs:/**", []string{"ANY"})
e := echo.New() e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/", nil) req := httptest.NewRequest(http.MethodGet, "/", nil)
@@ -141,9 +141,9 @@ func TestFindDomainFromFilesystem(t *testing.T) {
iam, err := getIAM() iam, err := getIAM()
require.NoError(t, err) require.NoError(t, err)
iam.AddPolicy("$anon", "$none", "fs:/**", "ANY") iam.AddPolicy("$anon", "$none", "fs:/**", []string{"ANY"})
iam.AddPolicy("foobar", "group", "fs:/group/**", "ANY") iam.AddPolicy("foobar", "group", "fs:/group/**", []string{"ANY"})
iam.AddPolicy("foobar", "anothergroup", "fs:/memfs/anothergroup/**", "ANY") iam.AddPolicy("foobar", "anothergroup", "fs:/memfs/anothergroup/**", []string{"ANY"})
mw := &iammiddleware{ mw := &iammiddleware{
iam: iam, iam: iam,
@@ -167,8 +167,8 @@ func TestBasicAuthDomain(t *testing.T) {
iam, err := getIAM() iam, err := getIAM()
require.NoError(t, err) require.NoError(t, err)
iam.AddPolicy("$anon", "$none", "fs:/**", "ANY") iam.AddPolicy("$anon", "$none", "fs:/**", []string{"ANY"})
iam.AddPolicy("foobar", "group", "fs:/group/**", "ANY") iam.AddPolicy("foobar", "group", "fs:/group/**", []string{"ANY"})
e := echo.New() e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/", nil) req := httptest.NewRequest(http.MethodGet, "/", nil)
@@ -200,7 +200,7 @@ func TestBasicAuthDomain(t *testing.T) {
require.NoError(t, h(c)) require.NoError(t, h(c))
// Allow anonymous group read access // Allow anonymous group read access
iam.AddPolicy("$anon", "group", "fs:/group/**", "GET") iam.AddPolicy("$anon", "group", "fs:/group/**", []string{"GET"})
req.Header.Del(echo.HeaderAuthorization) req.Header.Del(echo.HeaderAuthorization)
require.NoError(t, h(c)) require.NoError(t, h(c))
@@ -210,7 +210,7 @@ func TestAPILoginAndRefresh(t *testing.T) {
iam, err := getIAM() iam, err := getIAM()
require.NoError(t, err) require.NoError(t, err)
iam.AddPolicy("foobar", "$none", "api:/**", "ANY") iam.AddPolicy("foobar", "$none", "api:/**", []string{"ANY"})
jwthandler := apihandler.NewJWT(iam) jwthandler := apihandler.NewJWT(iam)

View File

@@ -66,8 +66,8 @@ func DummyRestreamer(pathPrefix string) (restream.Restreamer, error) {
return nil, err return nil, err
} }
iam.AddPolicy("$anon", "$none", "api:/**", "ANY") iam.AddPolicy("$anon", "$none", "api:/**", []string{"ANY"})
iam.AddPolicy("$anon", "$none", "fs:/**", "ANY") iam.AddPolicy("$anon", "$none", "fs:/**", []string{"ANY"})
rs, err := restream.New(restream.Config{ rs, err := restream.New(restream.Config{
Store: store, Store: store,

View File

@@ -126,6 +126,7 @@ type server struct {
session *api.SessionHandler session *api.SessionHandler
widget *api.WidgetHandler widget *api.WidgetHandler
resources *api.MetricsHandler resources *api.MetricsHandler
iam *api.IAMHandler
} }
middleware struct { middleware struct {
@@ -235,6 +236,8 @@ func NewServer(config Config) (Server, error) {
s.handler.jwt = api.NewJWT(config.IAM) s.handler.jwt = api.NewJWT(config.IAM)
s.v3handler.iam = api.NewIAM(config.IAM)
s.v3handler.log = api.NewLog( s.v3handler.log = api.NewLog(
config.LogBuffer, config.LogBuffer,
) )
@@ -528,6 +531,14 @@ func (s *server) setRoutesV3(v3 *echo.Group) {
s.router.GET("/api/v3/widget/process/:id", s.v3handler.widget.Get) s.router.GET("/api/v3/widget/process/:id", s.v3handler.widget.Get)
} }
// v3 IAM
if s.v3handler.iam != nil {
v3.POST("/iam/user", s.v3handler.iam.AddUser)
v3.GET("/iam/user/:name", s.v3handler.iam.GetUser)
v3.PUT("/iam/user/:name", s.v3handler.iam.UpdateUser)
v3.DELETE("/iam/user/:name", s.v3handler.iam.RemoveUser)
}
// v3 Restreamer // v3 Restreamer
if s.v3handler.restream != nil { if s.v3handler.restream != nil {
v3.GET("/skills", s.v3handler.restream.Skills) v3.GET("/skills", s.v3handler.restream.Skills)

View File

@@ -11,6 +11,13 @@ import (
"github.com/casbin/casbin/v2/model" "github.com/casbin/casbin/v2/model"
) )
type Policy struct {
Name string
Domain string
Resource string
Actions []string
}
type AccessEnforcer interface { type AccessEnforcer interface {
Enforce(name, domain, resource, action string) (bool, string) Enforce(name, domain, resource, action string) (bool, string)
HasGroup(name string) bool HasGroup(name string) bool
@@ -19,9 +26,9 @@ type AccessEnforcer interface {
type AccessManager interface { type AccessManager interface {
AccessEnforcer AccessEnforcer
AddPolicy(username, domain, resource, actions string) bool AddPolicy(name, domain, resource string, actions []string) bool
RemovePolicy(username, domain, resource, actions string) bool RemovePolicy(name, domain, resource string, actions []string) bool
ListPolicies(username, domain, resource, actions string) [][]string ListPolicies(name, domain, resource string, actions []string) []Policy
} }
type access struct { type access struct {
@@ -58,7 +65,10 @@ func NewAccessManager(config AccessConfig) (AccessManager, error) {
m.AddDef("e", "e", "some(where (p.eft == allow))") m.AddDef("e", "e", "some(where (p.eft == allow))")
m.AddDef("m", "m", `g(r.sub, p.sub, r.dom) && r.dom == p.dom && ResourceMatch(r.obj, r.dom, p.obj) && ActionMatch(r.act, p.act) || r.sub == "$superuser"`) m.AddDef("m", "m", `g(r.sub, p.sub, r.dom) && r.dom == p.dom && ResourceMatch(r.obj, r.dom, p.obj) && ActionMatch(r.act, p.act) || r.sub == "$superuser"`)
a := newAdapter(am.fs, "./policy.json", am.logger) a, err := newAdapter(am.fs, "./policy.json", am.logger)
if err != nil {
return nil, err
}
e, err := casbin.NewEnforcer(m, a) e, err := casbin.NewEnforcer(m, a)
if err != nil { if err != nil {
@@ -74,8 +84,8 @@ func NewAccessManager(config AccessConfig) (AccessManager, error) {
return am, nil return am, nil
} }
func (am *access) AddPolicy(username, domain, resource, actions string) bool { func (am *access) AddPolicy(name, domain, resource string, actions []string) bool {
policy := []string{username, domain, resource, actions} policy := []string{name, domain, resource, strings.Join(actions, "|")}
if am.enforcer.HasPolicy(policy) { if am.enforcer.HasPolicy(policy) {
return true return true
@@ -86,15 +96,28 @@ func (am *access) AddPolicy(username, domain, resource, actions string) bool {
return ok return ok
} }
func (am *access) RemovePolicy(username, domain, resource, actions string) bool { func (am *access) RemovePolicy(name, domain, resource string, actions []string) bool {
policies := am.enforcer.GetFilteredPolicy(0, username, domain, resource, actions) policies := am.enforcer.GetFilteredPolicy(0, name, domain, resource, strings.Join(actions, "|"))
am.enforcer.RemovePolicies(policies) am.enforcer.RemovePolicies(policies)
return true return true
} }
func (am *access) ListPolicies(username, domain, resource, actions string) [][]string { func (am *access) ListPolicies(name, domain, resource string, actions []string) []Policy {
return am.enforcer.GetFilteredPolicy(0, username, domain, resource, actions) policies := []Policy{}
ps := am.enforcer.GetFilteredPolicy(0, name, domain, resource, strings.Join(actions, "|"))
for _, p := range ps {
policies = append(policies, Policy{
Name: p[0],
Domain: p[1],
Resource: p[2],
Actions: strings.Split(p[3], "|"),
})
}
return policies
} }
func (am *access) HasGroup(name string) bool { func (am *access) HasGroup(name string) bool {

85
iam/access_test.go Normal file
View File

@@ -0,0 +1,85 @@
package iam
import (
"testing"
"github.com/datarhei/core/v16/io/fs"
"github.com/stretchr/testify/require"
)
func TestAccessManager(t *testing.T) {
memfs, err := fs.NewMemFilesystemFromDir("./fixtures", fs.MemConfig{})
require.NoError(t, err)
am, err := NewAccessManager(AccessConfig{
FS: memfs,
Logger: nil,
})
require.NoError(t, err)
policies := am.ListPolicies("", "", "", nil)
require.ElementsMatch(t, []Policy{
{
Name: "ingo",
Domain: "$none",
Resource: "rtmp:/bla-*",
Actions: []string{"play", "publish"},
},
{
Name: "ingo",
Domain: "igelcamp",
Resource: "rtmp:/igelcamp/**",
Actions: []string{"publish"},
},
}, policies)
am.AddPolicy("foobar", "group", "bla:/", []string{"write"})
policies = am.ListPolicies("", "", "", nil)
require.ElementsMatch(t, []Policy{
{
Name: "ingo",
Domain: "$none",
Resource: "rtmp:/bla-*",
Actions: []string{"play", "publish"},
},
{
Name: "ingo",
Domain: "igelcamp",
Resource: "rtmp:/igelcamp/**",
Actions: []string{"publish"},
},
{
Name: "foobar",
Domain: "group",
Resource: "bla:/",
Actions: []string{"write"},
},
}, policies)
require.True(t, am.HasGroup("igelcamp"))
require.True(t, am.HasGroup("group"))
require.False(t, am.HasGroup("$none"))
am.RemovePolicy("ingo", "", "", nil)
policies = am.ListPolicies("", "", "", nil)
require.ElementsMatch(t, []Policy{
{
Name: "foobar",
Domain: "group",
Resource: "bla:/",
Actions: []string{"write"},
},
}, policies)
require.False(t, am.HasGroup("igelcamp"))
require.True(t, am.HasGroup("group"))
require.False(t, am.HasGroup("$none"))
ok, _ := am.Enforce("foobar", "group", "bla:/", "read")
require.False(t, ok)
ok, _ = am.Enforce("foobar", "group", "bla:/", "write")
require.True(t, ok)
}

View File

@@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"sort"
"strings" "strings"
"sync" "sync"
@@ -23,12 +24,26 @@ type adapter struct {
lock sync.Mutex lock sync.Mutex
} }
func newAdapter(fs fs.Filesystem, filePath string, logger log.Logger) *adapter { func newAdapter(fs fs.Filesystem, filePath string, logger log.Logger) (*adapter, error) {
return &adapter{ a := &adapter{
fs: fs, fs: fs,
filePath: filePath, filePath: filePath,
logger: logger, logger: logger,
} }
if a.fs == nil {
return nil, fmt.Errorf("a filesystem has to be provided")
}
if len(a.filePath) == 0 {
return nil, fmt.Errorf("invalid file path, file path cannot be empty")
}
if a.logger == nil {
a.logger = log.New("")
}
return a, nil
} }
// Adapter // Adapter
@@ -36,10 +51,6 @@ func (a *adapter) LoadPolicy(model model.Model) error {
a.lock.Lock() a.lock.Lock()
defer a.lock.Unlock() defer a.lock.Unlock()
if a.filePath == "" {
return fmt.Errorf("invalid file path, file path cannot be empty")
}
return a.loadPolicyFile(model) return a.loadPolicyFile(model)
} }
@@ -69,7 +80,7 @@ func (a *adapter) loadPolicyFile(model model.Model) error {
rule[1] = "role:" + name rule[1] = "role:" + name
for _, role := range roles { for _, role := range roles {
rule[3] = role.Resource rule[3] = role.Resource
rule[4] = role.Actions rule[4] = formatActions(role.Actions)
if err := a.importPolicy(model, rule[0:5]); err != nil { if err := a.importPolicy(model, rule[0:5]); err != nil {
return err return err
@@ -80,7 +91,7 @@ func (a *adapter) loadPolicyFile(model model.Model) error {
for _, policy := range group.Policies { for _, policy := range group.Policies {
rule[1] = policy.Username rule[1] = policy.Username
rule[3] = policy.Resource rule[3] = policy.Resource
rule[4] = policy.Actions rule[4] = formatActions(policy.Actions)
if err := a.importPolicy(model, rule[0:5]); err != nil { if err := a.importPolicy(model, rule[0:5]); err != nil {
return err return err
@@ -138,10 +149,6 @@ func (a *adapter) SavePolicy(model model.Model) error {
} }
func (a *adapter) savePolicyFile() error { func (a *adapter) savePolicyFile() error {
if a.filePath == "" {
return fmt.Errorf("invalid file path, file path cannot be empty")
}
jsondata, err := json.MarshalIndent(a.groups, "", " ") jsondata, err := json.MarshalIndent(a.groups, "", " ")
if err != nil { if err != nil {
return err return err
@@ -201,7 +208,7 @@ func (a *adapter) addPolicy(ptype string, rule []string) error {
username = rule[0] username = rule[0]
domain = rule[1] domain = rule[1]
resource = rule[2] resource = rule[2]
actions = rule[3] actions = formatActions(rule[3])
a.logger.Debug().WithFields(log.Fields{ a.logger.Debug().WithFields(log.Fields{
"username": username, "username": username,
@@ -227,16 +234,20 @@ func (a *adapter) addPolicy(ptype string, rule []string) error {
for i := range a.groups { for i := range a.groups {
if a.groups[i].Name == domain { if a.groups[i].Name == domain {
group = &a.groups[i] group = &a.groups[i]
break
} }
} }
if group == nil { if group == nil {
g := Group{ g := Group{
Name: domain, Name: domain,
Roles: map[string][]Role{},
UserRoles: []MapUserRole{},
Policies: []GroupPolicy{},
} }
a.groups = append(a.groups, g) a.groups = append(a.groups, g)
group = &g group = &a.groups[len(a.groups)-1]
} }
if ptype == "p" { if ptype == "p" {
@@ -251,8 +262,8 @@ func (a *adapter) addPolicy(ptype string, rule []string) error {
Actions: actions, Actions: actions,
}) })
} else { } else {
group.Policies = append(group.Policies, Policy{ group.Policies = append(group.Policies, GroupPolicy{
Username: rule[0], Username: username,
Role: Role{ Role: Role{
Resource: resource, Resource: resource,
Actions: actions, Actions: actions,
@@ -284,7 +295,7 @@ func (a *adapter) hasPolicy(ptype string, rule []string) (bool, error) {
username = rule[0] username = rule[0]
domain = rule[1] domain = rule[1]
resource = rule[2] resource = rule[2]
actions = rule[3] actions = formatActions(rule[3])
} else if ptype == "g" { } else if ptype == "g" {
username = rule[0] username = rule[0]
role = rule[1] role = rule[1]
@@ -294,9 +305,9 @@ func (a *adapter) hasPolicy(ptype string, rule []string) (bool, error) {
} }
var group *Group = nil var group *Group = nil
for _, g := range a.groups { for i := range a.groups {
if g.Name == domain { if a.groups[i].Name == domain {
group = &g group = &a.groups[i]
break break
} }
} }
@@ -321,13 +332,13 @@ func (a *adapter) hasPolicy(ptype string, rule []string) (bool, error) {
} }
for _, role := range roles { for _, role := range roles {
if role.Resource == resource && role.Actions == actions { if role.Resource == resource && formatActions(role.Actions) == actions {
return true, nil return true, nil
} }
} }
} else { } else {
for _, p := range group.Policies { for _, p := range group.Policies {
if p.Username == username && p.Resource == resource && p.Actions == actions { if p.Username == username && p.Resource == resource && formatActions(p.Actions) == actions {
return true, nil return true, nil
} }
} }
@@ -393,7 +404,7 @@ func (a *adapter) removePolicy(ptype string, rule []string) error {
username = rule[0] username = rule[0]
domain = rule[1] domain = rule[1]
resource = rule[2] resource = rule[2]
actions = rule[3] actions = formatActions(rule[3])
a.logger.Debug().WithFields(log.Fields{ a.logger.Debug().WithFields(log.Fields{
"username": username, "username": username,
@@ -419,6 +430,7 @@ func (a *adapter) removePolicy(ptype string, rule []string) error {
for i := range a.groups { for i := range a.groups {
if a.groups[i].Name == domain { if a.groups[i].Name == domain {
group = &a.groups[i] group = &a.groups[i]
break
} }
} }
@@ -435,7 +447,7 @@ func (a *adapter) removePolicy(ptype string, rule []string) error {
newRoles := []Role{} newRoles := []Role{}
for _, role := range roles { for _, role := range roles {
if role.Resource == resource && role.Actions == actions { if role.Resource == resource && formatActions(role.Actions) == actions {
continue continue
} }
@@ -444,10 +456,10 @@ func (a *adapter) removePolicy(ptype string, rule []string) error {
group.Roles[username] = newRoles group.Roles[username] = newRoles
} else { } else {
policies := []Policy{} policies := []GroupPolicy{}
for _, p := range group.Policies { for _, p := range group.Policies {
if p.Username == username && p.Resource == resource && p.Actions == actions { if p.Username == username && p.Resource == resource && formatActions(p.Actions) == actions {
continue continue
} }
@@ -472,6 +484,21 @@ func (a *adapter) removePolicy(ptype string, rule []string) error {
group.UserRoles = users group.UserRoles = users
} }
// Remove the group if there are no rules and policies
if len(group.Roles) == 0 && len(group.UserRoles) == 0 && len(group.Policies) == 0 {
groups := []Group{}
for _, g := range a.groups {
if g.Name == group.Name {
continue
}
groups = append(groups, g)
}
a.groups = groups
}
return nil return nil
} }
@@ -498,7 +525,7 @@ type Group struct {
Name string `json:"name"` Name string `json:"name"`
Roles map[string][]Role `json:"roles"` Roles map[string][]Role `json:"roles"`
UserRoles []MapUserRole `json:"userroles"` UserRoles []MapUserRole `json:"userroles"`
Policies []Policy `json:"policies"` Policies []GroupPolicy `json:"policies"`
} }
type Role struct { type Role struct {
@@ -511,7 +538,15 @@ type MapUserRole struct {
Role string `json:"role"` Role string `json:"role"`
} }
type Policy struct { type GroupPolicy struct {
Username string `json:"username"` Username string `json:"username"`
Role Role
} }
func formatActions(actions string) string {
a := strings.Split(actions, "|")
sort.Strings(a)
return strings.Join(a, "|")
}

87
iam/adapter_test.go Normal file
View File

@@ -0,0 +1,87 @@
package iam
import (
"encoding/json"
"testing"
"github.com/datarhei/core/v16/io/fs"
"github.com/stretchr/testify/require"
)
func TestAddPolicy(t *testing.T) {
memfs, err := fs.NewMemFilesystem(fs.MemConfig{})
require.NoError(t, err)
a, err := newAdapter(memfs, "/policy.json", nil)
require.NoError(t, err)
err = a.AddPolicy("p", "p", []string{"foobar", "group", "resource", "action"})
require.NoError(t, err)
require.Equal(t, 1, len(a.groups))
data, err := memfs.ReadFile("/policy.json")
require.NoError(t, err)
g := []Group{}
err = json.Unmarshal(data, &g)
require.NoError(t, err)
require.Equal(t, "group", g[0].Name)
require.Equal(t, 1, len(g[0].Policies))
require.Equal(t, GroupPolicy{
Username: "foobar",
Role: Role{
Resource: "resource",
Actions: "action",
},
}, g[0].Policies[0])
}
func TestFormatActions(t *testing.T) {
data := [][]string{
{"a|b|c", "a|b|c"},
{"b|c|a", "a|b|c"},
}
for _, d := range data {
require.Equal(t, d[1], formatActions(d[0]), d[0])
}
}
func TestRemovePolicy(t *testing.T) {
memfs, err := fs.NewMemFilesystem(fs.MemConfig{})
require.NoError(t, err)
a, err := newAdapter(memfs, "/policy.json", nil)
require.NoError(t, err)
err = a.AddPolicies("p", "p", [][]string{
{"foobar1", "group", "resource1", "action1"},
{"foobar2", "group", "resource2", "action2"},
})
require.NoError(t, err)
require.Equal(t, 1, len(a.groups))
require.Equal(t, 2, len(a.groups[0].Policies))
err = a.RemovePolicy("p", "p", []string{"foobar1", "group", "resource1", "action1"})
require.NoError(t, err)
require.Equal(t, 1, len(a.groups))
require.Equal(t, 1, len(a.groups[0].Policies))
err = a.RemovePolicy("p", "p", []string{"foobar2", "group", "resource2", "action2"})
require.NoError(t, err)
require.Equal(t, 0, len(a.groups))
data, err := memfs.ReadFile("/policy.json")
require.NoError(t, err)
g := []Group{}
err = json.Unmarshal(data, &g)
require.NoError(t, err)
require.Equal(t, 0, len(g))
}

26
iam/fixtures/policy.json Normal file
View File

@@ -0,0 +1,26 @@
[
{
"name": "$none",
"roles": {},
"userroles": [],
"policies": [
{
"username": "ingo",
"resource": "rtmp:/bla-*",
"actions": "play|publish"
}
]
},
{
"name": "igelcamp",
"roles": null,
"userroles": null,
"policies": [
{
"username": "ingo",
"resource": "rtmp:/igelcamp/**",
"actions": "publish"
}
]
}
]

View File

@@ -9,21 +9,23 @@ type IAM interface {
Enforce(user, domain, resource, action string) bool Enforce(user, domain, resource, action string) bool
HasDomain(domain string) bool HasDomain(domain string) bool
AddPolicy(username, domain, resource, actions string) bool AddPolicy(username, domain, resource string, actions []string) bool
RemovePolicy(username, domain, resource, actions string) bool RemovePolicy(username, domain, resource string, actions []string) bool
ListPolicies(username, domain, resource, actions string) [][]string ListPolicies(username, domain, resource string, actions []string) []Policy
Validators() []string Validators() []string
CreateIdentity(u User) error CreateIdentity(u User) error
GetIdentity(name string) (User, error)
UpdateIdentity(name string, u User) error UpdateIdentity(name string, u User) error
DeleteIdentity(name string) error DeleteIdentity(name string) error
ListIdentities() []User ListIdentities() []User
SaveIdentities() error
GetIdentity(name string) (IdentityVerifier, error) GetVerifier(name string) (IdentityVerifier, error)
GetIdentityByAuth0(name string) (IdentityVerifier, error) GetVerfierFromAuth0(name string) (IdentityVerifier, error)
GetDefaultIdentity() (IdentityVerifier, error) GetDefaultVerifier() (IdentityVerifier, error)
CreateJWT(name string) (string, string, error) CreateJWT(name string) (string, string, error)
@@ -85,17 +87,25 @@ func (i *iam) Close() {
i.am = nil i.am = nil
} }
func (i *iam) Enforce(user, domain, resource, action string) bool { func (i *iam) Enforce(name, domain, resource, action string) bool {
if len(name) == 0 {
name = "$anon"
}
if len(domain) == 0 {
domain = "$none"
}
superuser := false superuser := false
if identity, err := i.im.GetVerifier(user); err == nil { if identity, err := i.im.GetVerifier(name); err == nil {
if identity.IsSuperuser() { if identity.IsSuperuser() {
superuser = true superuser = true
} }
} }
l := i.logger.Debug().WithFields(log.Fields{ l := i.logger.Debug().WithFields(log.Fields{
"subject": user, "subject": name,
"domain": domain, "domain": domain,
"resource": resource, "resource": resource,
"action": action, "action": action,
@@ -103,10 +113,10 @@ func (i *iam) Enforce(user, domain, resource, action string) bool {
}) })
if superuser { if superuser {
user = "$superuser" name = "$superuser"
} }
ok, rule := i.am.Enforce(user, domain, resource, action) ok, rule := i.am.Enforce(name, domain, resource, action)
if !ok { if !ok {
l.Log("no match") l.Log("no match")
@@ -121,6 +131,10 @@ func (i *iam) CreateIdentity(u User) error {
return i.im.Create(u) return i.im.Create(u)
} }
func (i *iam) GetIdentity(name string) (User, error) {
return i.im.Get(name)
}
func (i *iam) UpdateIdentity(name string, u User) error { func (i *iam) UpdateIdentity(name string, u User) error {
return i.im.Update(name, u) return i.im.Update(name, u)
} }
@@ -133,15 +147,19 @@ func (i *iam) ListIdentities() []User {
return nil return nil
} }
func (i *iam) GetIdentity(name string) (IdentityVerifier, error) { func (i *iam) SaveIdentities() error {
return i.im.Save()
}
func (i *iam) GetVerifier(name string) (IdentityVerifier, error) {
return i.im.GetVerifier(name) return i.im.GetVerifier(name)
} }
func (i *iam) GetIdentityByAuth0(name string) (IdentityVerifier, error) { func (i *iam) GetVerfierFromAuth0(name string) (IdentityVerifier, error) {
return i.im.GetVerifierByAuth0(name) return i.im.GetVerifierFromAuth0(name)
} }
func (i *iam) GetDefaultIdentity() (IdentityVerifier, error) { func (i *iam) GetDefaultVerifier() (IdentityVerifier, error) {
return i.im.GetDefaultVerifier() return i.im.GetDefaultVerifier()
} }
@@ -157,14 +175,22 @@ func (i *iam) Validators() []string {
return i.im.Validators() return i.im.Validators()
} }
func (i *iam) AddPolicy(username, domain, resource, actions string) bool { func (i *iam) AddPolicy(name, domain, resource string, actions []string) bool {
return i.am.AddPolicy(username, domain, resource, actions) if len(name) == 0 {
name = "$anon"
}
if len(domain) == 0 {
domain = "$none"
}
return i.am.AddPolicy(name, domain, resource, actions)
} }
func (i *iam) RemovePolicy(username, domain, resource, actions string) bool { func (i *iam) RemovePolicy(name, domain, resource string, actions []string) bool {
return i.am.RemovePolicy(username, domain, resource, actions) return i.am.RemovePolicy(name, domain, resource, actions)
} }
func (i *iam) ListPolicies(username, domain, resource, actions string) [][]string { func (i *iam) ListPolicies(name, domain, resource string, actions []string) []Policy {
return i.am.ListPolicies(username, domain, resource, actions) return i.am.ListPolicies(name, domain, resource, actions)
} }

View File

@@ -378,7 +378,7 @@ type IdentityManager interface {
Get(name string) (User, error) Get(name string) (User, error)
GetVerifier(name string) (IdentityVerifier, error) GetVerifier(name string) (IdentityVerifier, error)
GetVerifierByAuth0(name string) (IdentityVerifier, error) GetVerifierFromAuth0(name string) (IdentityVerifier, error)
GetDefaultVerifier() (IdentityVerifier, error) GetDefaultVerifier() (IdentityVerifier, error)
Validators() []string Validators() []string
@@ -697,7 +697,7 @@ func (im *identityManager) GetVerifier(name string) (IdentityVerifier, error) {
return im.getIdentity(name) return im.getIdentity(name)
} }
func (im *identityManager) GetVerifierByAuth0(name string) (IdentityVerifier, error) { func (im *identityManager) GetVerifierFromAuth0(name string) (IdentityVerifier, error) {
im.lock.RLock() im.lock.RLock()
defer im.lock.RUnlock() defer im.lock.RUnlock()
@@ -841,9 +841,9 @@ func (im *identityManager) CreateJWT(name string) (string, string, error) {
} }
type Auth0Tenant struct { type Auth0Tenant struct {
Domain string Domain string `json:"domain"`
Audience string Audience string `json:"audience"`
ClientID string ClientID string `json:"client_id"`
} }
func (t *Auth0Tenant) key() string { func (t *Auth0Tenant) key() string {

View File

@@ -353,11 +353,11 @@ func TestCreateUserAuth0(t *testing.T) {
}) })
require.NoError(t, err) require.NoError(t, err)
identity, err := im.GetVerifierByAuth0("foobaz") identity, err := im.GetVerifierFromAuth0("foobaz")
require.Error(t, err) require.Error(t, err)
require.Nil(t, identity) require.Nil(t, identity)
identity, err = im.GetVerifierByAuth0("auth0|123456") identity, err = im.GetVerifierFromAuth0("auth0|123456")
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, identity) require.NotNil(t, identity)
@@ -553,7 +553,7 @@ func TestUpdateUserAuth0(t *testing.T) {
}) })
require.NoError(t, err) require.NoError(t, err)
identity, err := im.GetVerifierByAuth0("auth0|123456") identity, err := im.GetVerifierFromAuth0("auth0|123456")
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, identity) require.NotNil(t, identity)
@@ -569,7 +569,7 @@ func TestUpdateUserAuth0(t *testing.T) {
err = im.Update("foobaz", user) err = im.Update("foobaz", user)
require.NoError(t, err) require.NoError(t, err)
identity, err = im.GetVerifierByAuth0("auth0|123456") identity, err = im.GetVerifierFromAuth0("auth0|123456")
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, identity) require.NotNil(t, identity)

1
io/fs/fixtures/a.txt Normal file
View File

@@ -0,0 +1 @@
qwertz

1
io/fs/fixtures/b.txt Normal file
View File

@@ -0,0 +1 @@
qwertz

View File

@@ -156,6 +156,8 @@ func NewMemFilesystemFromDir(dir string, config MemConfig) (Filesystem, error) {
return nil, err return nil, err
} }
dir = filepath.Clean(dir)
err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
return nil return nil
@@ -181,7 +183,7 @@ func NewMemFilesystemFromDir(dir string, config MemConfig) (Filesystem, error) {
defer file.Close() defer file.Close()
_, _, err = mem.WriteFileReader(path, file) _, _, err = mem.WriteFileReader(strings.TrimPrefix(path, dir), file)
if err != nil { if err != nil {
return fmt.Errorf("can't copy %s", path) return fmt.Errorf("can't copy %s", path)
} }

View File

@@ -7,24 +7,16 @@ import (
) )
func TestMemFromDir(t *testing.T) { func TestMemFromDir(t *testing.T) {
mem, err := NewMemFilesystemFromDir(".", MemConfig{}) mem, err := NewMemFilesystemFromDir("./fixtures", MemConfig{})
require.NoError(t, err) require.NoError(t, err)
names := []string{} names := []string{}
for _, f := range mem.List("/", "/*.go") { for _, f := range mem.List("/", "") {
names = append(names, f.Name()) names = append(names, f.Name())
} }
require.ElementsMatch(t, []string{ require.ElementsMatch(t, []string{
"/disk.go", "/a.txt",
"/fs_test.go", "/b.txt",
"/fs.go",
"/mem_test.go",
"/mem.go",
"/readonly_test.go",
"/readonly.go",
"/s3.go",
"/sized_test.go",
"/sized.go",
}, names) }, names)
} }

View File

@@ -424,7 +424,7 @@ func (r *restream) enforce(name, group, processid, action string) bool {
if len(name) == 0 { if len(name) == 0 {
// This is for backwards compatibility. Existing processes don't have an owner. // This is for backwards compatibility. Existing processes don't have an owner.
// All processes that will be added later will have an owner ($anon, ...). // All processes that will be added later will have an owner ($anon, ...).
identity, err := r.iam.GetDefaultIdentity() identity, err := r.iam.GetDefaultVerifier()
if err != nil { if err != nil {
name = "$anon" name = "$anon"
} else { } else {
@@ -914,7 +914,7 @@ func (r *restream) resolveAddress(tasks map[string]*task, id, address string) (s
return address, fmt.Errorf("unknown process '%s' in group '%s' (%s)", matches["id"], matches["group"], address) return address, fmt.Errorf("unknown process '%s' in group '%s' (%s)", matches["id"], matches["group"], address)
} }
identity, _ := r.iam.GetIdentity(t.config.Owner) identity, _ := r.iam.GetVerifier(t.config.Owner)
teeOptions := regexp.MustCompile(`^\[[^\]]*\]`) teeOptions := regexp.MustCompile(`^\[[^\]]*\]`)

View File

@@ -51,7 +51,7 @@ func getDummyRestreamer(portrange net.Portranger, validatorIn, validatorOut ffmp
return nil, err return nil, err
} }
iam.AddPolicy("$anon", "$none", "process:*", "CREATE|GET|DELETE|UPDATE|COMMAND|PROBE|METADATA|PLAYOUT") iam.AddPolicy("$anon", "$none", "process:*", []string{"CREATE", "GET", "DELETE", "UPDATE", "COMMAND", "PROBE", "METADATA", "PLAYOUT"})
rewriter, err := rewrite.New(rewrite.Config{}) rewriter, err := rewrite.New(rewrite.Config{})
if err != nil { if err != nil {

View File

@@ -3,7 +3,6 @@ package rtmp
import ( import (
"context" "context"
"net" "net"
"net/url"
"sync" "sync"
"time" "time"
@@ -94,11 +93,11 @@ type channel struct {
isProxy bool isProxy bool
} }
func newChannel(conn connection, u *url.URL, reference string, remote net.Addr, streams []av.CodecData, isProxy bool, collector session.Collector) *channel { func newChannel(conn connection, playpath, reference string, remote net.Addr, streams []av.CodecData, isProxy bool, collector session.Collector) *channel {
ch := &channel{ ch := &channel{
path: u.Path, path: playpath,
reference: reference, reference: reference,
publisher: newClient(conn, u.Path, collector), publisher: newClient(conn, playpath, collector),
subscriber: make(map[string]*client), subscriber: make(map[string]*client),
collector: collector, collector: collector,
streams: streams, streams: streams,

View File

@@ -106,7 +106,7 @@ func New(config Config) (Server, error) {
} }
s := &server{ s := &server{
app: config.App, app: filepath.Join("/", config.App),
token: config.Token, token: config.Token,
logger: config.Logger, logger: config.Logger,
collector: config.Collector, collector: config.Collector,
@@ -184,19 +184,19 @@ func (s *server) Channels() []string {
return channels return channels
} }
func (s *server) log(who, action, path, message string, client net.Addr) { func (s *server) log(who, what, action, path, message string, client net.Addr) {
s.logger.Info().WithFields(log.Fields{ s.logger.Info().WithFields(log.Fields{
"who": who, "who": who,
"what": what,
"action": action, "action": action,
"path": path, "path": path,
"client": client.String(), "client": client.String(),
}).Log(message) }).Log(message)
} }
// GetToken returns the path and the token found in the URL. If the token // GetToken returns the path without the token and the token found in the URL. If the token
// was part of the path, the token is removed from the path. The token in // was part of the path, the token is removed from the path. The token in the query string
// the query string takes precedence. The token in the path is assumed to // takes precedence. The token in the path is assumed to be the last path element.
// be the last path element.
func GetToken(u *url.URL) (string, string) { func GetToken(u *url.URL) (string, string) {
q := u.Query() q := u.Query()
if q.Has("token") { if q.Has("token") {
@@ -204,15 +204,34 @@ func GetToken(u *url.URL) (string, string) {
return u.Path, q.Get("token") return u.Path, q.Get("token")
} }
pathElements := strings.Split(u.EscapedPath(), "/") pathElements := splitPath(u.EscapedPath())
nPathElements := len(pathElements) nPathElements := len(pathElements)
if nPathElements == 0 { if nPathElements <= 1 {
return u.Path, "" return u.Path, ""
} }
// Return the path without the token // Return the path without the token
return strings.Join(pathElements[:nPathElements-1], "/"), pathElements[nPathElements-1] return "/" + strings.Join(pathElements[:nPathElements-1], "/"), pathElements[nPathElements-1]
}
func splitPath(path string) []string {
pathElements := strings.Split(filepath.Clean(path), "/")
if len(pathElements) == 0 {
return pathElements
}
if len(pathElements[0]) == 0 {
pathElements = pathElements[1:]
}
return pathElements
}
func removePathPrefix(path, prefix string) (string, string) {
prefix = filepath.Join("/", prefix)
return filepath.Join("/", strings.TrimPrefix(path, prefix+"/")), prefix
} }
// handlePlay is called when a RTMP client wants to play a stream // handlePlay is called when a RTMP client wants to play a stream
@@ -220,20 +239,22 @@ func (s *server) handlePlay(conn *rtmp.Conn) {
defer conn.Close() defer conn.Close()
remote := conn.NetConn().RemoteAddr() remote := conn.NetConn().RemoteAddr()
playPath, token := GetToken(conn.URL) playpath, token := GetToken(conn.URL)
playpath, _ = removePathPrefix(playpath, s.app)
identity, err := s.findIdentityFromStreamKey(token) identity, err := s.findIdentityFromStreamKey(token)
if err != nil { if err != nil {
s.logger.Debug().WithError(err).Log("invalid streamkey") s.logger.Debug().WithError(err).Log("invalid streamkey")
s.log("PLAY", "FORBIDDEN", playPath, "invalid streamkey ("+token+")", remote) s.log(identity, "PLAY", "FORBIDDEN", playpath, "invalid streamkey ("+token+")", remote)
return return
} }
domain := s.findDomainFromPlaypath(playPath) domain := s.findDomainFromPlaypath(playpath)
resource := "rtmp:" + playPath resource := "rtmp:" + playpath
if !s.iam.Enforce(identity, domain, resource, "PLAY") { if !s.iam.Enforce(identity, domain, resource, "PLAY") {
s.log("PLAY", "FORBIDDEN", playPath, "access denied", remote) s.log(identity, "PLAY", "FORBIDDEN", playpath, "access denied", remote)
return return
} }
@@ -258,14 +279,14 @@ func (s *server) handlePlay(conn *rtmp.Conn) {
// Look for the stream // Look for the stream
s.lock.RLock() s.lock.RLock()
ch := s.channels[playPath] ch := s.channels[playpath]
s.lock.RUnlock() s.lock.RUnlock()
if ch != nil { if ch != nil {
// Send the metadata to the client // Send the metadata to the client
conn.WriteHeader(ch.streams) conn.WriteHeader(ch.streams)
s.log("PLAY", "START", conn.URL.Path, "", remote) s.log(identity, "PLAY", "START", conn.URL.Path, "", remote)
// Get a cursor and apply filters // Get a cursor and apply filters
cursor := ch.queue.Oldest() cursor := ch.queue.Oldest()
@@ -292,9 +313,9 @@ func (s *server) handlePlay(conn *rtmp.Conn) {
ch.RemoveSubscriber(id) ch.RemoveSubscriber(id)
s.log("PLAY", "STOP", playPath, "", remote) s.log(identity, "PLAY", "STOP", playpath, "", remote)
} else { } else {
s.log("PLAY", "NOTFOUND", playPath, "", remote) s.log(identity, "PLAY", "NOTFOUND", playpath, "", remote)
} }
} }
@@ -303,52 +324,54 @@ func (s *server) handlePublish(conn *rtmp.Conn) {
defer conn.Close() defer conn.Close()
remote := conn.NetConn().RemoteAddr() remote := conn.NetConn().RemoteAddr()
playPath, token := GetToken(conn.URL) playpath, token := GetToken(conn.URL)
// Check the app patch playpath, app := removePathPrefix(playpath, s.app)
if !strings.HasPrefix(playPath, s.app) {
s.log("PUBLISH", "FORBIDDEN", conn.URL.Path, "invalid app", remote)
return
}
identity, err := s.findIdentityFromStreamKey(token) identity, err := s.findIdentityFromStreamKey(token)
if err != nil { if err != nil {
s.logger.Debug().WithError(err).Log("invalid streamkey") s.logger.Debug().WithError(err).Log("invalid streamkey")
s.log("PUBLISH", "FORBIDDEN", playPath, "invalid streamkey ("+token+")", remote) s.log(identity, "PUBLISH", "FORBIDDEN", playpath, "invalid streamkey ("+token+")", remote)
return return
} }
domain := s.findDomainFromPlaypath(playPath) // Check the app patch
resource := "rtmp:" + playPath if app != s.app {
s.log(identity, "PUBLISH", "FORBIDDEN", playpath, "invalid app", remote)
return
}
domain := s.findDomainFromPlaypath(playpath)
resource := "rtmp:" + playpath
if !s.iam.Enforce(identity, domain, resource, "PUBLISH") { if !s.iam.Enforce(identity, domain, resource, "PUBLISH") {
s.log("PUBLISH", "FORBIDDEN", playPath, "access denied", remote) s.log(identity, "PUBLISH", "FORBIDDEN", playpath, "access denied", remote)
return return
} }
err = s.publish(conn, conn.URL, remote, false) err = s.publish(conn, playpath, remote, identity, false)
if err != nil { if err != nil {
s.logger.WithField("path", conn.URL.Path).WithError(err).Log("") s.logger.WithField("path", conn.URL.Path).WithError(err).Log("")
} }
} }
func (s *server) publish(src connection, u *url.URL, remote net.Addr, isProxy bool) error { func (s *server) publish(src connection, playpath string, remote net.Addr, identity string, isProxy bool) error {
// Check the streams if it contains any valid/known streams // Check the streams if it contains any valid/known streams
streams, _ := src.Streams() streams, _ := src.Streams()
if len(streams) == 0 { if len(streams) == 0 {
s.log("PUBLISH", "INVALID", u.Path, "no streams available", remote) s.log(identity, "PUBLISH", "INVALID", playpath, "no streams available", remote)
return fmt.Errorf("no streams are available") return fmt.Errorf("no streams are available")
} }
s.lock.Lock() s.lock.Lock()
ch := s.channels[u.Path] ch := s.channels[playpath]
if ch == nil { if ch == nil {
reference := strings.TrimPrefix(strings.TrimSuffix(u.Path, filepath.Ext(u.Path)), s.app+"/") reference := strings.TrimPrefix(strings.TrimSuffix(playpath, filepath.Ext(playpath)), s.app+"/")
// Create a new channel // Create a new channel
ch = newChannel(src, u, reference, remote, streams, isProxy, s.collector) ch = newChannel(src, playpath, reference, remote, streams, isProxy, s.collector)
for _, stream := range streams { for _, stream := range streams {
typ := stream.Type() typ := stream.Type()
@@ -361,7 +384,7 @@ func (s *server) publish(src connection, u *url.URL, remote net.Addr, isProxy bo
} }
} }
s.channels[u.Path] = ch s.channels[playpath] = ch
} else { } else {
ch = nil ch = nil
} }
@@ -369,26 +392,26 @@ func (s *server) publish(src connection, u *url.URL, remote net.Addr, isProxy bo
s.lock.Unlock() s.lock.Unlock()
if ch == nil { if ch == nil {
s.log("PUBLISH", "CONFLICT", u.Path, "already publishing", remote) s.log(identity, "PUBLISH", "CONFLICT", playpath, "already publishing", remote)
return fmt.Errorf("already publishing") return fmt.Errorf("already publishing")
} }
s.log("PUBLISH", "START", u.Path, "", remote) s.log(identity, "PUBLISH", "START", playpath, "", remote)
for _, stream := range streams { for _, stream := range streams {
s.log("PUBLISH", "STREAM", u.Path, stream.Type().String(), remote) s.log(identity, "PUBLISH", "STREAM", playpath, stream.Type().String(), remote)
} }
// Ingest the data, blocks until done // Ingest the data, blocks until done
avutil.CopyPackets(ch.queue, src) avutil.CopyPackets(ch.queue, src)
s.lock.Lock() s.lock.Lock()
delete(s.channels, u.Path) delete(s.channels, playpath)
s.lock.Unlock() s.lock.Unlock()
ch.Close() ch.Close()
s.log("PUBLISH", "STOP", u.Path, "", remote) s.log(identity, "PUBLISH", "STOP", playpath, "", remote)
return nil return nil
} }
@@ -405,10 +428,10 @@ func (s *server) findIdentityFromStreamKey(key string) (string, error) {
elements := strings.Split(key, ":") elements := strings.Split(key, ":")
if len(elements) == 1 { if len(elements) == 1 {
identity, err = s.iam.GetDefaultIdentity() identity, err = s.iam.GetDefaultVerifier()
token = elements[0] token = elements[0]
} else { } else {
identity, err = s.iam.GetIdentity(elements[0]) identity, err = s.iam.GetVerifier(elements[0])
token = elements[1] token = elements[1]
} }
@@ -423,10 +446,12 @@ func (s *server) findIdentityFromStreamKey(key string) (string, error) {
return identity.Name(), nil return identity.Name(), nil
} }
// findDomainFromPlaypath finds the domain in the path. The domain is
// the first path element. If there's only one path element, it is not
// considered the domain. It is assumed that the app is not part of
// the provided path.
func (s *server) findDomainFromPlaypath(path string) string { func (s *server) findDomainFromPlaypath(path string) string {
path = strings.TrimPrefix(path, filepath.Join(s.app, "/")) elements := splitPath(path)
elements := strings.Split(path, "/")
if len(elements) == 1 { if len(elements) == 1 {
return "$none" return "$none"
} }

View File

@@ -24,3 +24,31 @@ func TestToken(t *testing.T) {
require.Equal(t, d[2], token, "url=%s", u.String()) require.Equal(t, d[2], token, "url=%s", u.String())
} }
} }
func TestSplitPath(t *testing.T) {
data := map[string][]string{
"/foo/bar": {"foo", "bar"},
"foo/bar": {"foo", "bar"},
"/foo/bar/": {"foo", "bar"},
}
for path, split := range data {
elms := splitPath(path)
require.ElementsMatch(t, split, elms, "%s", path)
}
}
func TestRemovePathPrefix(t *testing.T) {
data := [][]string{
{"/foo/bar", "/foo", "/bar"},
{"/foo/bar", "/fo", "/foo/bar"},
{"/foo/bar/abc", "/foo/bar", "/abc"},
}
for _, d := range data {
x, _ := removePathPrefix(d[0], d[1])
require.Equal(t, d[2], x, "path=%s prefix=%s", d[0], d[1])
}
}

View File

@@ -387,10 +387,10 @@ func (s *server) findIdentityFromToken(key string) (string, error) {
elements := strings.Split(key, ":") elements := strings.Split(key, ":")
if len(elements) == 1 { if len(elements) == 1 {
identity, err = s.iam.GetDefaultIdentity() identity, err = s.iam.GetDefaultVerifier()
token = elements[0] token = elements[0]
} else { } else {
identity, err = s.iam.GetIdentity(elements[0]) identity, err = s.iam.GetVerifier(elements[0])
token = elements[1] token = elements[1]
} }