diff --git a/app/api/api.go b/app/api/api.go index 86570d63..f4c7ca6f 100644 --- a/app/api/api.go +++ b/app/api/api.go @@ -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 { diff --git a/app/import/import.go b/app/import/import.go index 58817554..18a1ec2e 100644 --- a/app/import/import.go +++ b/app/import/import.go @@ -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, diff --git a/docs/docs.go b/docs/docs.go index 353c8b58..05431c32 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -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" }, diff --git a/docs/swagger.json b/docs/swagger.json index 75d15a44..e380b060 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -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" }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index d735b7c8..9481a3f2 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -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 diff --git a/http/api/iam.go b/http/api/iam.go new file mode 100644 index 00000000..7f3ad9a6 --- /dev/null +++ b/http/api/iam.go @@ -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"` +} diff --git a/http/handler/api/iam.go b/http/handler/api/iam.go new file mode 100644 index 00000000..740b11f9 --- /dev/null +++ b/http/handler/api/iam.go @@ -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) +} diff --git a/http/middleware/iam/iam.go b/http/middleware/iam/iam.go index cea85ea5..8620f6cf 100644 --- a/http/middleware/iam/iam.go +++ b/http/middleware/iam/iam.go @@ -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, diff --git a/http/middleware/iam/iam_test.go b/http/middleware/iam/iam_test.go index 3127e2af..ae5afd26 100644 --- a/http/middleware/iam/iam_test.go +++ b/http/middleware/iam/iam_test.go @@ -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) diff --git a/http/mock/mock.go b/http/mock/mock.go index fddd31fc..94cdb65e 100644 --- a/http/mock/mock.go +++ b/http/mock/mock.go @@ -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, diff --git a/http/server.go b/http/server.go index 09855c75..00fcb406 100644 --- a/http/server.go +++ b/http/server.go @@ -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) diff --git a/iam/access.go b/iam/access.go index fb4e264f..353c2339 100644 --- a/iam/access.go +++ b/iam/access.go @@ -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 { diff --git a/iam/access_test.go b/iam/access_test.go new file mode 100644 index 00000000..3dc30779 --- /dev/null +++ b/iam/access_test.go @@ -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) +} diff --git a/iam/adapter.go b/iam/adapter.go index b2cf132b..524925a1 100644 --- a/iam/adapter.go +++ b/iam/adapter.go @@ -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, "|") +} diff --git a/iam/adapter_test.go b/iam/adapter_test.go new file mode 100644 index 00000000..de80be84 --- /dev/null +++ b/iam/adapter_test.go @@ -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)) +} diff --git a/iam/fixtures/policy.json b/iam/fixtures/policy.json new file mode 100644 index 00000000..306467ef --- /dev/null +++ b/iam/fixtures/policy.json @@ -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" + } + ] + } +] \ No newline at end of file diff --git a/iam/iam.go b/iam/iam.go index 19fe0628..a1c97565 100644 --- a/iam/iam.go +++ b/iam/iam.go @@ -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) } diff --git a/iam/identity.go b/iam/identity.go index ead90ba5..18d5d2c8 100644 --- a/iam/identity.go +++ b/iam/identity.go @@ -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 { diff --git a/iam/identity_test.go b/iam/identity_test.go index 69d7a1b9..f1798dc5 100644 --- a/iam/identity_test.go +++ b/iam/identity_test.go @@ -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) diff --git a/io/fs/fixtures/a.txt b/io/fs/fixtures/a.txt new file mode 100644 index 00000000..ea3377dc --- /dev/null +++ b/io/fs/fixtures/a.txt @@ -0,0 +1 @@ +qwertz \ No newline at end of file diff --git a/io/fs/fixtures/b.txt b/io/fs/fixtures/b.txt new file mode 100644 index 00000000..ea3377dc --- /dev/null +++ b/io/fs/fixtures/b.txt @@ -0,0 +1 @@ +qwertz \ No newline at end of file diff --git a/io/fs/mem.go b/io/fs/mem.go index a75eb932..3f9681e7 100644 --- a/io/fs/mem.go +++ b/io/fs/mem.go @@ -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) } diff --git a/io/fs/mem_test.go b/io/fs/mem_test.go index d28a0d92..d8b3fa1f 100644 --- a/io/fs/mem_test.go +++ b/io/fs/mem_test.go @@ -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) } diff --git a/restream/restream.go b/restream/restream.go index 6ad4aba4..fc0c1507 100644 --- a/restream/restream.go +++ b/restream/restream.go @@ -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(`^\[[^\]]*\]`) diff --git a/restream/restream_test.go b/restream/restream_test.go index fa2e9158..56b341cb 100644 --- a/restream/restream_test.go +++ b/restream/restream_test.go @@ -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 { diff --git a/rtmp/channel.go b/rtmp/channel.go index 9ea68fe3..72ce5737 100644 --- a/rtmp/channel.go +++ b/rtmp/channel.go @@ -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, diff --git a/rtmp/rtmp.go b/rtmp/rtmp.go index dc569831..d7f50756 100644 --- a/rtmp/rtmp.go +++ b/rtmp/rtmp.go @@ -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" } diff --git a/rtmp/rtmp_test.go b/rtmp/rtmp_test.go index 37848afc..ec372690 100644 --- a/rtmp/rtmp_test.go +++ b/rtmp/rtmp_test.go @@ -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]) + } +} diff --git a/srt/srt.go b/srt/srt.go index 374672a8..d8166b34 100644 --- a/srt/srt.go +++ b/srt/srt.go @@ -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] }