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
// the behaviour before IAM
iam.RemovePolicy("$anon", "$none", "", "")
iam.RemovePolicy("$localhost", "$none", "", "")
iam.RemovePolicy("$anon", "$none", "", nil)
iam.RemovePolicy("$localhost", "$none", "", nil)
iam.AddPolicy("$anon", "$none", "fs:/**", "GET|HEAD|OPTIONS")
iam.AddPolicy("$anon", "$none", "api:/api", "GET|HEAD|OPTIONS")
iam.AddPolicy("$anon", "$none", "api:/api/v3/widget/process/**", "GET|HEAD|OPTIONS")
iam.AddPolicy("$anon", "$none", "fs:/**", []string{"GET", "HEAD", "OPTIONS"})
iam.AddPolicy("$anon", "$none", "api:/api", []string{"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/v3/widget/process/**", "GET|HEAD|OPTIONS")
iam.AddPolicy("$localhost", "$none", "api:/api", []string{"GET", "HEAD", "OPTIONS"})
iam.AddPolicy("$localhost", "$none", "api:/api/v3/widget/process/**", []string{"GET", "HEAD", "OPTIONS"})
if !cfg.API.Auth.Enable {
iam.AddPolicy("$anon", "$none", "api:/api/**", "ANY")
iam.AddPolicy("$anon", "$none", "process:*", "ANY")
iam.AddPolicy("$localhost", "$none", "api:/api/**", "ANY")
iam.AddPolicy("$localhost", "$none", "process:*", "ANY")
iam.AddPolicy("$anon", "$none", "api:/api/**", []string{"ANY"})
iam.AddPolicy("$anon", "$none", "process:*", []string{"ANY"})
iam.AddPolicy("$localhost", "$none", "api:/api/**", []string{"ANY"})
iam.AddPolicy("$localhost", "$none", "process:*", []string{"ANY"})
} else {
if cfg.API.Auth.DisableLocalhost {
iam.AddPolicy("$localhost", "$none", "api:/api/**", "ANY")
iam.AddPolicy("$localhost", "$none", "process:*", "ANY")
iam.AddPolicy("$localhost", "$none", "api:/api/**", []string{"ANY"})
iam.AddPolicy("$localhost", "$none", "process:*", []string{"ANY"})
}
}
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 {
iam.AddPolicy("$anon", "$none", "rtmp:/**", "ANY")
iam.AddPolicy("$anon", "$none", "rtmp:/**", []string{"ANY"})
}
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
@@ -672,9 +672,9 @@ func (a *api) start() error {
var identity iam.IdentityVerifier = nil
if len(config.Owner) == 0 {
identity, _ = a.iam.GetDefaultIdentity()
identity, _ = a.iam.GetDefaultVerifier()
} else {
identity, _ = a.iam.GetIdentity(config.Owner)
identity, _ = a.iam.GetVerifier(config.Owner)
}
if identity != nil {
@@ -698,9 +698,9 @@ func (a *api) start() error {
var identity iam.IdentityVerifier = nil
if len(config.Owner) == 0 {
identity, _ = a.iam.GetDefaultIdentity()
identity, _ = a.iam.GetDefaultVerifier()
} else {
identity, _ = a.iam.GetIdentity(config.Owner)
identity, _ = a.iam.GetVerifier(config.Owner)
}
if identity != nil {

View File

@@ -1440,7 +1440,7 @@ func probeInput(binary string, config app.Config) app.Probe {
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{
FFmpeg: ffmpeg,

View File

@@ -1,5 +1,4 @@
// Package docs GENERATED BY SWAG; DO NOT EDIT
// This file was generated by swaggo/swag
// Code generated by swaggo/swag. DO NOT EDIT
package docs
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": {
"get": {
"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": {
"get": {
"security": [
@@ -2690,22 +2809,115 @@ const docTemplate = `{
}
}
},
"api.JWT": {
"api.IAMAuth0Tenant": {
"type": "object",
"properties": {
"access_token": {
"audience": {
"type": "string"
},
"refresh_token": {
"client_id": {
"type": "string"
},
"domain": {
"type": "string"
}
}
},
"api.JWTRefresh": {
"api.IAMPolicy": {
"type": "object",
"properties": {
"access_token": {
"actions": {
"type": "array",
"items": {
"type": "string"
}
},
"group": {
"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",
"additionalProperties": true
},
"api.Login": {
"type": "object",
"required": [
"password",
"username"
],
"properties": {
"password": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"api.MetricsDescription": {
"type": "object",
"properties": {
@@ -3043,6 +3240,9 @@ const docTemplate = `{
"autostart": {
"type": "boolean"
},
"group": {
"type": "string"
},
"id": {
"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": {
"get": {
"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": {
"get": {
"security": [
@@ -2682,22 +2802,115 @@
}
}
},
"api.JWT": {
"api.IAMAuth0Tenant": {
"type": "object",
"properties": {
"access_token": {
"audience": {
"type": "string"
},
"refresh_token": {
"client_id": {
"type": "string"
},
"domain": {
"type": "string"
}
}
},
"api.JWTRefresh": {
"api.IAMPolicy": {
"type": "object",
"properties": {
"access_token": {
"actions": {
"type": "array",
"items": {
"type": "string"
}
},
"group": {
"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",
"additionalProperties": true
},
"api.Login": {
"type": "object",
"required": [
"password",
"username"
],
"properties": {
"password": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"api.MetricsDescription": {
"type": "object",
"properties": {
@@ -3035,6 +3233,9 @@
"autostart": {
"type": "boolean"
},
"group": {
"type": "string"
},
"id": {
"type": "string"
},

View File

@@ -469,31 +469,81 @@ definitions:
items: {}
type: array
type: object
api.JWT:
api.IAMAuth0Tenant:
properties:
access_token:
audience:
type: string
refresh_token:
client_id:
type: string
domain:
type: string
type: object
api.JWTRefresh:
api.IAMPolicy:
properties:
access_token:
actions:
items:
type: string
type: array
group:
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
api.LogEvent:
additionalProperties: true
type: object
api.Login:
properties:
password:
type: string
username:
type: string
required:
- password
- username
type: object
api.MetricsDescription:
properties:
description:
@@ -706,6 +756,8 @@ definitions:
properties:
autostart:
type: boolean
group:
type: string
id:
type: string
input:
@@ -1980,58 +2032,6 @@ paths:
security:
- ApiKeyAuth: []
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:
get:
description: Swagger UI for this API
@@ -2266,6 +2266,135 @@ paths:
security:
- ApiKeyAuth: []
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:
get:
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 {
m.logger.Debug().WithFields(log.Fields{
"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 {
m.logger.Debug().WithFields(log.Fields{
"path": c.Request().URL.Path,
@@ -343,7 +343,7 @@ func (m *iammiddleware) findIdentityFromUserpass(c echo.Context) (iam.IdentityVe
return nil, nil
}
identity, err := m.iam.GetIdentity(login.Username)
identity, err := m.iam.GetVerifier(login.Username)
if err != nil {
m.logger.Debug().WithFields(log.Fields{
"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 {
m.logger.Debug().WithFields(log.Fields{
"path": c.Request().URL.Path,

View File

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

View File

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

View File

@@ -126,6 +126,7 @@ type server struct {
session *api.SessionHandler
widget *api.WidgetHandler
resources *api.MetricsHandler
iam *api.IAMHandler
}
middleware struct {
@@ -235,6 +236,8 @@ func NewServer(config Config) (Server, error) {
s.handler.jwt = api.NewJWT(config.IAM)
s.v3handler.iam = api.NewIAM(config.IAM)
s.v3handler.log = api.NewLog(
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)
}
// 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
if s.v3handler.restream != nil {
v3.GET("/skills", s.v3handler.restream.Skills)

View File

@@ -11,6 +11,13 @@ import (
"github.com/casbin/casbin/v2/model"
)
type Policy struct {
Name string
Domain string
Resource string
Actions []string
}
type AccessEnforcer interface {
Enforce(name, domain, resource, action string) (bool, string)
HasGroup(name string) bool
@@ -19,9 +26,9 @@ type AccessEnforcer interface {
type AccessManager interface {
AccessEnforcer
AddPolicy(username, domain, resource, actions string) bool
RemovePolicy(username, domain, resource, actions string) bool
ListPolicies(username, domain, resource, actions string) [][]string
AddPolicy(name, domain, resource string, actions []string) bool
RemovePolicy(name, domain, resource string, actions []string) bool
ListPolicies(name, domain, resource string, actions []string) []Policy
}
type access struct {
@@ -58,7 +65,10 @@ func NewAccessManager(config AccessConfig) (AccessManager, error) {
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"`)
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)
if err != nil {
@@ -74,8 +84,8 @@ func NewAccessManager(config AccessConfig) (AccessManager, error) {
return am, nil
}
func (am *access) AddPolicy(username, domain, resource, actions string) bool {
policy := []string{username, domain, resource, actions}
func (am *access) AddPolicy(name, domain, resource string, actions []string) bool {
policy := []string{name, domain, resource, strings.Join(actions, "|")}
if am.enforcer.HasPolicy(policy) {
return true
@@ -86,15 +96,28 @@ func (am *access) AddPolicy(username, domain, resource, actions string) bool {
return ok
}
func (am *access) RemovePolicy(username, domain, resource, actions string) bool {
policies := am.enforcer.GetFilteredPolicy(0, username, domain, resource, actions)
func (am *access) RemovePolicy(name, domain, resource string, actions []string) bool {
policies := am.enforcer.GetFilteredPolicy(0, name, domain, resource, strings.Join(actions, "|"))
am.enforcer.RemovePolicies(policies)
return true
}
func (am *access) ListPolicies(username, domain, resource, actions string) [][]string {
return am.enforcer.GetFilteredPolicy(0, username, domain, resource, actions)
func (am *access) ListPolicies(name, domain, resource string, actions []string) []Policy {
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 {

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"
"fmt"
"os"
"sort"
"strings"
"sync"
@@ -23,12 +24,26 @@ type adapter struct {
lock sync.Mutex
}
func newAdapter(fs fs.Filesystem, filePath string, logger log.Logger) *adapter {
return &adapter{
func newAdapter(fs fs.Filesystem, filePath string, logger log.Logger) (*adapter, error) {
a := &adapter{
fs: fs,
filePath: filePath,
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
@@ -36,10 +51,6 @@ func (a *adapter) LoadPolicy(model model.Model) error {
a.lock.Lock()
defer a.lock.Unlock()
if a.filePath == "" {
return fmt.Errorf("invalid file path, file path cannot be empty")
}
return a.loadPolicyFile(model)
}
@@ -69,7 +80,7 @@ func (a *adapter) loadPolicyFile(model model.Model) error {
rule[1] = "role:" + name
for _, role := range roles {
rule[3] = role.Resource
rule[4] = role.Actions
rule[4] = formatActions(role.Actions)
if err := a.importPolicy(model, rule[0:5]); err != nil {
return err
@@ -80,7 +91,7 @@ func (a *adapter) loadPolicyFile(model model.Model) error {
for _, policy := range group.Policies {
rule[1] = policy.Username
rule[3] = policy.Resource
rule[4] = policy.Actions
rule[4] = formatActions(policy.Actions)
if err := a.importPolicy(model, rule[0:5]); err != nil {
return err
@@ -138,10 +149,6 @@ func (a *adapter) SavePolicy(model model.Model) 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, "", " ")
if err != nil {
return err
@@ -201,7 +208,7 @@ func (a *adapter) addPolicy(ptype string, rule []string) error {
username = rule[0]
domain = rule[1]
resource = rule[2]
actions = rule[3]
actions = formatActions(rule[3])
a.logger.Debug().WithFields(log.Fields{
"username": username,
@@ -227,16 +234,20 @@ func (a *adapter) addPolicy(ptype string, rule []string) error {
for i := range a.groups {
if a.groups[i].Name == domain {
group = &a.groups[i]
break
}
}
if group == nil {
g := Group{
Name: domain,
Name: domain,
Roles: map[string][]Role{},
UserRoles: []MapUserRole{},
Policies: []GroupPolicy{},
}
a.groups = append(a.groups, g)
group = &g
group = &a.groups[len(a.groups)-1]
}
if ptype == "p" {
@@ -251,8 +262,8 @@ func (a *adapter) addPolicy(ptype string, rule []string) error {
Actions: actions,
})
} else {
group.Policies = append(group.Policies, Policy{
Username: rule[0],
group.Policies = append(group.Policies, GroupPolicy{
Username: username,
Role: Role{
Resource: resource,
Actions: actions,
@@ -284,7 +295,7 @@ func (a *adapter) hasPolicy(ptype string, rule []string) (bool, error) {
username = rule[0]
domain = rule[1]
resource = rule[2]
actions = rule[3]
actions = formatActions(rule[3])
} else if ptype == "g" {
username = rule[0]
role = rule[1]
@@ -294,9 +305,9 @@ func (a *adapter) hasPolicy(ptype string, rule []string) (bool, error) {
}
var group *Group = nil
for _, g := range a.groups {
if g.Name == domain {
group = &g
for i := range a.groups {
if a.groups[i].Name == domain {
group = &a.groups[i]
break
}
}
@@ -321,13 +332,13 @@ func (a *adapter) hasPolicy(ptype string, rule []string) (bool, error) {
}
for _, role := range roles {
if role.Resource == resource && role.Actions == actions {
if role.Resource == resource && formatActions(role.Actions) == actions {
return true, nil
}
}
} else {
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
}
}
@@ -393,7 +404,7 @@ func (a *adapter) removePolicy(ptype string, rule []string) error {
username = rule[0]
domain = rule[1]
resource = rule[2]
actions = rule[3]
actions = formatActions(rule[3])
a.logger.Debug().WithFields(log.Fields{
"username": username,
@@ -419,6 +430,7 @@ func (a *adapter) removePolicy(ptype string, rule []string) error {
for i := range a.groups {
if a.groups[i].Name == domain {
group = &a.groups[i]
break
}
}
@@ -435,7 +447,7 @@ func (a *adapter) removePolicy(ptype string, rule []string) error {
newRoles := []Role{}
for _, role := range roles {
if role.Resource == resource && role.Actions == actions {
if role.Resource == resource && formatActions(role.Actions) == actions {
continue
}
@@ -444,10 +456,10 @@ func (a *adapter) removePolicy(ptype string, rule []string) error {
group.Roles[username] = newRoles
} else {
policies := []Policy{}
policies := []GroupPolicy{}
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
}
@@ -472,6 +484,21 @@ func (a *adapter) removePolicy(ptype string, rule []string) error {
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
}
@@ -498,7 +525,7 @@ type Group struct {
Name string `json:"name"`
Roles map[string][]Role `json:"roles"`
UserRoles []MapUserRole `json:"userroles"`
Policies []Policy `json:"policies"`
Policies []GroupPolicy `json:"policies"`
}
type Role struct {
@@ -511,7 +538,15 @@ type MapUserRole struct {
Role string `json:"role"`
}
type Policy struct {
type GroupPolicy struct {
Username string `json:"username"`
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
HasDomain(domain string) bool
AddPolicy(username, domain, resource, actions string) bool
RemovePolicy(username, domain, resource, actions string) bool
AddPolicy(username, domain, resource string, 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
CreateIdentity(u User) error
GetIdentity(name string) (User, error)
UpdateIdentity(name string, u User) error
DeleteIdentity(name string) error
ListIdentities() []User
SaveIdentities() error
GetIdentity(name string) (IdentityVerifier, error)
GetIdentityByAuth0(name string) (IdentityVerifier, error)
GetDefaultIdentity() (IdentityVerifier, error)
GetVerifier(name string) (IdentityVerifier, error)
GetVerfierFromAuth0(name string) (IdentityVerifier, error)
GetDefaultVerifier() (IdentityVerifier, error)
CreateJWT(name string) (string, string, error)
@@ -85,17 +87,25 @@ func (i *iam) Close() {
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
if identity, err := i.im.GetVerifier(user); err == nil {
if identity, err := i.im.GetVerifier(name); err == nil {
if identity.IsSuperuser() {
superuser = true
}
}
l := i.logger.Debug().WithFields(log.Fields{
"subject": user,
"subject": name,
"domain": domain,
"resource": resource,
"action": action,
@@ -103,10 +113,10 @@ func (i *iam) Enforce(user, domain, resource, action string) bool {
})
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 {
l.Log("no match")
@@ -121,6 +131,10 @@ func (i *iam) CreateIdentity(u User) error {
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 {
return i.im.Update(name, u)
}
@@ -133,15 +147,19 @@ func (i *iam) ListIdentities() []User {
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)
}
func (i *iam) GetIdentityByAuth0(name string) (IdentityVerifier, error) {
return i.im.GetVerifierByAuth0(name)
func (i *iam) GetVerfierFromAuth0(name string) (IdentityVerifier, error) {
return i.im.GetVerifierFromAuth0(name)
}
func (i *iam) GetDefaultIdentity() (IdentityVerifier, error) {
func (i *iam) GetDefaultVerifier() (IdentityVerifier, error) {
return i.im.GetDefaultVerifier()
}
@@ -157,14 +175,22 @@ func (i *iam) Validators() []string {
return i.im.Validators()
}
func (i *iam) AddPolicy(username, domain, resource, actions string) bool {
return i.am.AddPolicy(username, domain, resource, actions)
func (i *iam) AddPolicy(name, domain, resource string, actions []string) bool {
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 {
return i.am.RemovePolicy(username, domain, resource, actions)
func (i *iam) RemovePolicy(name, domain, resource string, actions []string) bool {
return i.am.RemovePolicy(name, domain, resource, actions)
}
func (i *iam) ListPolicies(username, domain, resource, actions string) [][]string {
return i.am.ListPolicies(username, domain, resource, actions)
func (i *iam) ListPolicies(name, domain, resource string, actions []string) []Policy {
return i.am.ListPolicies(name, domain, resource, actions)
}

View File

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

View File

@@ -353,11 +353,11 @@ func TestCreateUserAuth0(t *testing.T) {
})
require.NoError(t, err)
identity, err := im.GetVerifierByAuth0("foobaz")
identity, err := im.GetVerifierFromAuth0("foobaz")
require.Error(t, err)
require.Nil(t, identity)
identity, err = im.GetVerifierByAuth0("auth0|123456")
identity, err = im.GetVerifierFromAuth0("auth0|123456")
require.NoError(t, err)
require.NotNil(t, identity)
@@ -553,7 +553,7 @@ func TestUpdateUserAuth0(t *testing.T) {
})
require.NoError(t, err)
identity, err := im.GetVerifierByAuth0("auth0|123456")
identity, err := im.GetVerifierFromAuth0("auth0|123456")
require.NoError(t, err)
require.NotNil(t, identity)
@@ -569,7 +569,7 @@ func TestUpdateUserAuth0(t *testing.T) {
err = im.Update("foobaz", user)
require.NoError(t, err)
identity, err = im.GetVerifierByAuth0("auth0|123456")
identity, err = im.GetVerifierFromAuth0("auth0|123456")
require.NoError(t, err)
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
}
dir = filepath.Clean(dir)
err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return nil
@@ -181,7 +183,7 @@ func NewMemFilesystemFromDir(dir string, config MemConfig) (Filesystem, error) {
defer file.Close()
_, _, err = mem.WriteFileReader(path, file)
_, _, err = mem.WriteFileReader(strings.TrimPrefix(path, dir), file)
if err != nil {
return fmt.Errorf("can't copy %s", path)
}

View File

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

View File

@@ -424,7 +424,7 @@ func (r *restream) enforce(name, group, processid, action string) bool {
if len(name) == 0 {
// This is for backwards compatibility. Existing processes don't have an owner.
// 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 {
name = "$anon"
} 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)
}
identity, _ := r.iam.GetIdentity(t.config.Owner)
identity, _ := r.iam.GetVerifier(t.config.Owner)
teeOptions := regexp.MustCompile(`^\[[^\]]*\]`)

View File

@@ -51,7 +51,7 @@ func getDummyRestreamer(portrange net.Portranger, validatorIn, validatorOut ffmp
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{})
if err != nil {

View File

@@ -3,7 +3,6 @@ package rtmp
import (
"context"
"net"
"net/url"
"sync"
"time"
@@ -94,11 +93,11 @@ type channel struct {
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{
path: u.Path,
path: playpath,
reference: reference,
publisher: newClient(conn, u.Path, collector),
publisher: newClient(conn, playpath, collector),
subscriber: make(map[string]*client),
collector: collector,
streams: streams,

View File

@@ -106,7 +106,7 @@ func New(config Config) (Server, error) {
}
s := &server{
app: config.App,
app: filepath.Join("/", config.App),
token: config.Token,
logger: config.Logger,
collector: config.Collector,
@@ -184,19 +184,19 @@ func (s *server) Channels() []string {
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{
"who": who,
"what": what,
"action": action,
"path": path,
"client": client.String(),
}).Log(message)
}
// GetToken returns the path 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
// the query string takes precedence. The token in the path is assumed to
// be the last path element.
// 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 the query string
// takes precedence. The token in the path is assumed to be the last path element.
func GetToken(u *url.URL) (string, string) {
q := u.Query()
if q.Has("token") {
@@ -204,15 +204,34 @@ func GetToken(u *url.URL) (string, string) {
return u.Path, q.Get("token")
}
pathElements := strings.Split(u.EscapedPath(), "/")
pathElements := splitPath(u.EscapedPath())
nPathElements := len(pathElements)
if nPathElements == 0 {
if nPathElements <= 1 {
return u.Path, ""
}
// 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
@@ -220,20 +239,22 @@ func (s *server) handlePlay(conn *rtmp.Conn) {
defer conn.Close()
remote := conn.NetConn().RemoteAddr()
playPath, token := GetToken(conn.URL)
playpath, token := GetToken(conn.URL)
playpath, _ = removePathPrefix(playpath, s.app)
identity, err := s.findIdentityFromStreamKey(token)
if err != nil {
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
}
domain := s.findDomainFromPlaypath(playPath)
resource := "rtmp:" + playPath
domain := s.findDomainFromPlaypath(playpath)
resource := "rtmp:" + playpath
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
}
@@ -258,14 +279,14 @@ func (s *server) handlePlay(conn *rtmp.Conn) {
// Look for the stream
s.lock.RLock()
ch := s.channels[playPath]
ch := s.channels[playpath]
s.lock.RUnlock()
if ch != nil {
// Send the metadata to the client
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
cursor := ch.queue.Oldest()
@@ -292,9 +313,9 @@ func (s *server) handlePlay(conn *rtmp.Conn) {
ch.RemoveSubscriber(id)
s.log("PLAY", "STOP", playPath, "", remote)
s.log(identity, "PLAY", "STOP", playpath, "", remote)
} 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()
remote := conn.NetConn().RemoteAddr()
playPath, token := GetToken(conn.URL)
playpath, token := GetToken(conn.URL)
// Check the app patch
if !strings.HasPrefix(playPath, s.app) {
s.log("PUBLISH", "FORBIDDEN", conn.URL.Path, "invalid app", remote)
return
}
playpath, app := removePathPrefix(playpath, s.app)
identity, err := s.findIdentityFromStreamKey(token)
if err != nil {
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
}
domain := s.findDomainFromPlaypath(playPath)
resource := "rtmp:" + playPath
// Check the app patch
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") {
s.log("PUBLISH", "FORBIDDEN", playPath, "access denied", remote)
s.log(identity, "PUBLISH", "FORBIDDEN", playpath, "access denied", remote)
return
}
err = s.publish(conn, conn.URL, remote, false)
err = s.publish(conn, playpath, remote, identity, false)
if err != nil {
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
streams, _ := src.Streams()
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")
}
s.lock.Lock()
ch := s.channels[u.Path]
ch := s.channels[playpath]
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
ch = newChannel(src, u, reference, remote, streams, isProxy, s.collector)
ch = newChannel(src, playpath, reference, remote, streams, isProxy, s.collector)
for _, stream := range streams {
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 {
ch = nil
}
@@ -369,26 +392,26 @@ func (s *server) publish(src connection, u *url.URL, remote net.Addr, isProxy bo
s.lock.Unlock()
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")
}
s.log("PUBLISH", "START", u.Path, "", remote)
s.log(identity, "PUBLISH", "START", playpath, "", remote)
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
avutil.CopyPackets(ch.queue, src)
s.lock.Lock()
delete(s.channels, u.Path)
delete(s.channels, playpath)
s.lock.Unlock()
ch.Close()
s.log("PUBLISH", "STOP", u.Path, "", remote)
s.log(identity, "PUBLISH", "STOP", playpath, "", remote)
return nil
}
@@ -405,10 +428,10 @@ func (s *server) findIdentityFromStreamKey(key string) (string, error) {
elements := strings.Split(key, ":")
if len(elements) == 1 {
identity, err = s.iam.GetDefaultIdentity()
identity, err = s.iam.GetDefaultVerifier()
token = elements[0]
} else {
identity, err = s.iam.GetIdentity(elements[0])
identity, err = s.iam.GetVerifier(elements[0])
token = elements[1]
}
@@ -423,10 +446,12 @@ func (s *server) findIdentityFromStreamKey(key string) (string, error) {
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 {
path = strings.TrimPrefix(path, filepath.Join(s.app, "/"))
elements := strings.Split(path, "/")
elements := splitPath(path)
if len(elements) == 1 {
return "$none"
}

View File

@@ -24,3 +24,31 @@ func TestToken(t *testing.T) {
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, ":")
if len(elements) == 1 {
identity, err = s.iam.GetDefaultIdentity()
identity, err = s.iam.GetDefaultVerifier()
token = elements[0]
} else {
identity, err = s.iam.GetIdentity(elements[0])
identity, err = s.iam.GetVerifier(elements[0])
token = elements[1]
}