* feat(playback): add per-user transcoding controls * fix(playback): enforce forced video transcode permission * chore: address transcode control review feedback * fix(playback): recheck transcode permission on audio switch --------- Co-authored-by: Quick104 <31828688+Quick104@users.noreply.github.com>
195 lines
9.2 KiB
Go
195 lines
9.2 KiB
Go
package policy
|
|
|
|
const (
|
|
// ActionDownload checks whether a user may create or serve original downloads.
|
|
ActionDownload = "download"
|
|
// ActionDownloadTranscode checks whether a user may request transcode-backed downloads.
|
|
ActionDownloadTranscode = "download_transcode"
|
|
// ActionPlaybackAdmission checks whether a playback session may be admitted.
|
|
ActionPlaybackAdmission = "playback_admission"
|
|
|
|
// RequestedActionDirectPlay is the playback admission fact for non-transcode playback.
|
|
RequestedActionDirectPlay = "direct_play"
|
|
// RequestedActionTranscode is the playback admission fact for transcode playback.
|
|
RequestedActionTranscode = "transcode"
|
|
// RequestedActionAudioTranscode is the playback admission fact for copy-video, transcoded-audio playback.
|
|
RequestedActionAudioTranscode = "audio_transcode"
|
|
|
|
// PermissionActingAdmin is the pseudo-permission used for acting-admin gates.
|
|
PermissionActingAdmin = "acting_admin"
|
|
// PermissionMarkerEdit mirrors auth.PermissionMarkerEdit.
|
|
PermissionMarkerEdit = "marker_edit"
|
|
// PermissionMetadataCuration mirrors auth.PermissionMetadataCuration.
|
|
PermissionMetadataCuration = "metadata_curation"
|
|
)
|
|
|
|
// ScopeInput is the policy input document for resolving an authenticated
|
|
// viewer request into an effective access scope.
|
|
//
|
|
// Library lists retain the server-side nil-vs-empty distinction through the
|
|
// account_restricted and profile_library_restricted booleans. A nil or empty
|
|
// account_library_ids slice is not enough for policy authors to infer whether
|
|
// the account is unrestricted.
|
|
type ScopeInput struct {
|
|
SchemaVersion int `json:"schema_version"`
|
|
|
|
UserID int `json:"user_id"`
|
|
SessionID string `json:"session_id"`
|
|
ProfileID string `json:"profile_id"`
|
|
AccountLibraryIDs []int `json:"account_library_ids"`
|
|
AccountRestricted bool `json:"account_restricted"`
|
|
AccountMaxQuality string `json:"account_max_playback_quality"`
|
|
AccessPolicyRevision int64 `json:"access_policy_revision"`
|
|
DisabledLibraryIDs []int `json:"disabled_library_ids"`
|
|
ProfilePresent bool `json:"profile_present"`
|
|
ProfileMaxRating string `json:"profile_max_content_rating"`
|
|
ProfileMaxQuality string `json:"profile_max_playback_quality"`
|
|
ProfileLibraryLimited bool `json:"profile_library_restricted"`
|
|
ProfileLibraryIDs []int `json:"profile_allowed_library_ids"`
|
|
ProfileHasPIN bool `json:"profile_has_pin"`
|
|
ProfileVerified bool `json:"profile_verified"`
|
|
ProfileMetadataLang string `json:"profile_preferred_metadata_language"`
|
|
|
|
RequestTime string `json:"request_time"`
|
|
DeviceID string `json:"device_id"`
|
|
ClientIP string `json:"client_ip"`
|
|
IsAPIKey bool `json:"is_api_key"`
|
|
}
|
|
|
|
// ScopeDecision is the policy output document for viewer scope resolution.
|
|
//
|
|
// Rego and JSON cannot preserve Go's nil-vs-empty slice semantics, so
|
|
// unrestricted explicitly records whether allowed_library_ids is meaningful.
|
|
// Adapters map unrestricted=true to a nil access.Scope.AllowedLibraryIDs.
|
|
type ScopeDecision struct {
|
|
SchemaVersion int `json:"schema_version"`
|
|
Unrestricted bool `json:"unrestricted"`
|
|
AllowedLibraryIDs []int `json:"allowed_library_ids"`
|
|
DisabledLibraryIDs []int `json:"disabled_library_ids"`
|
|
LibrariesRestricted bool `json:"libraries_restricted"`
|
|
MaxContentRating string `json:"max_content_rating"`
|
|
MaxPlaybackQuality string `json:"max_playback_quality"`
|
|
PreferredMetadataLanguage string `json:"preferred_metadata_language"`
|
|
PolicyRevision int64 `json:"policy_revision"`
|
|
ProfileVerified bool `json:"profile_verified"`
|
|
}
|
|
|
|
// PermissionInput is the policy input document for route-level permission
|
|
// gates.
|
|
//
|
|
// acting_as_primary is precomputed in Go from the declared profile because
|
|
// Rego never performs database lookups. user_libraries_restricted distinguishes
|
|
// nil user library assignment (unrestricted) from an empty allowlist.
|
|
type PermissionInput struct {
|
|
SchemaVersion int `json:"schema_version"`
|
|
|
|
UserID int `json:"user_id"`
|
|
Role string `json:"role"`
|
|
UserEnabled bool `json:"user_enabled"`
|
|
AssignedPermissions []string `json:"assigned_permissions"`
|
|
Permission string `json:"permission"`
|
|
DeclaredProfileID string `json:"declared_profile_id"`
|
|
ActingAsPrimary bool `json:"acting_as_primary"`
|
|
TargetLibraryIDs []int `json:"target_library_ids"`
|
|
UserLibraryIDs []int `json:"user_library_ids"`
|
|
UserLibrariesRestricted bool `json:"user_libraries_restricted"`
|
|
|
|
RequestTime string `json:"request_time"`
|
|
DeviceID string `json:"device_id"`
|
|
ClientIP string `json:"client_ip"`
|
|
}
|
|
|
|
// PermissionDecision is the policy output document for route-level permission
|
|
// gates. Reason is human-readable free text; ReasonCode is the stable machine
|
|
// contract (one of the ReasonCode* constants).
|
|
type PermissionDecision struct {
|
|
Allowed bool `json:"allowed"`
|
|
Reason string `json:"reason"`
|
|
ReasonCode string `json:"reason_code"`
|
|
}
|
|
|
|
// Reason codes emitted by the vendor policy bundle. These — not the free-text
|
|
// reason strings — are the contract Go consumers branch on. A custom override
|
|
// that flips a base allow to a deny always carries ReasonCodeCustomDenial,
|
|
// because override reasons are administrator free text.
|
|
const (
|
|
ReasonCodeCustomDenial = "custom_denial"
|
|
|
|
// Permission decisions.
|
|
ReasonCodeUnknownPermission = "unknown_permission"
|
|
ReasonCodeUserDisabled = "user_disabled"
|
|
ReasonCodeAdminRoleRequired = "admin_role_required"
|
|
ReasonCodePrimaryProfileRequired = "primary_profile_required"
|
|
ReasonCodeMarkerEditPermissionRequired = "marker_edit_permission_required"
|
|
ReasonCodeMetadataCurationRequired = "metadata_curation_permission_required"
|
|
ReasonCodeItemOutsideUserLibraries = "item_outside_user_libraries"
|
|
|
|
// Action decisions.
|
|
ReasonCodeUnknownAction = "unknown_action"
|
|
ReasonCodeDownloadsDisabled = "downloads_disabled"
|
|
ReasonCodeDownloadPermissionRequired = "download_permission_required"
|
|
ReasonCodeTranscodeDisabled = "transcode_disabled"
|
|
ReasonCodeDownloadTranscodeRequired = "download_transcode_permission_required"
|
|
ReasonCodeDownloadArtifactsUnavailable = "download_artifacts_unavailable"
|
|
ReasonCodeQualityCeilingExceeded = "quality_ceiling_exceeded"
|
|
ReasonCodeContentRatingExceeded = "content_rating_exceeded"
|
|
ReasonCodeMaxStreamsExceeded = "max_streams_exceeded"
|
|
ReasonCodeMaxTranscodesExceeded = "max_transcodes_exceeded"
|
|
ReasonCodeTranscodingDisabled = "transcoding_disabled"
|
|
ReasonCodeAudioTranscodingDisabled = "audio_transcoding_disabled"
|
|
)
|
|
|
|
// ActionInput is the policy input document for download eligibility,
|
|
// download-transcode eligibility, and playback admission.
|
|
//
|
|
// Go supplies all dynamic facts: live playback counts, user flags, and config
|
|
// flags. Policy never reads server configuration or session state directly.
|
|
type ActionInput struct {
|
|
SchemaVersion int `json:"schema_version"`
|
|
|
|
Action string `json:"action"`
|
|
UserID int `json:"user_id"`
|
|
DownloadAllowed bool `json:"download_allowed"`
|
|
DownloadTranscodeAllowed bool `json:"download_transcode_allowed"`
|
|
MaxStreams int `json:"max_streams"`
|
|
MaxTranscodes int `json:"max_transcodes"`
|
|
TranscodeAllowed bool `json:"transcode_allowed"`
|
|
AudioTranscodeAllowed bool `json:"audio_transcode_allowed"`
|
|
|
|
DownloadsEnabled bool `json:"downloads_enabled"`
|
|
TranscodeEnabled bool `json:"transcode_enabled"`
|
|
ArtifactsAvailable bool `json:"artifacts_available"`
|
|
|
|
CurrentActiveStreams int `json:"current_active_streams"`
|
|
CurrentActiveTranscodes int `json:"current_active_transcodes"`
|
|
RequestedAction string `json:"requested_action"`
|
|
|
|
// FileQuality is supplied only when the source resolution is what gets
|
|
// served: direct-original and remux downloads. Capped transcode checks and
|
|
// playback admission leave it empty (the vendor quality gate treats empty
|
|
// as allowed), so custom rules keyed on file_quality only fire for
|
|
// original-resolution serving. The content-rating pair is likewise empty
|
|
// on download paths — rating ceilings are enforced by the access filter at
|
|
// item access before any action check runs.
|
|
RequestedQuality string `json:"requested_quality"`
|
|
FileQuality string `json:"file_quality"`
|
|
MaxPlaybackQuality string `json:"max_playback_quality"`
|
|
ContentRating string `json:"content_rating"`
|
|
MaxContentRating string `json:"max_content_rating"`
|
|
|
|
RequestTime string `json:"request_time"`
|
|
DeviceID string `json:"device_id"`
|
|
ClientIP string `json:"client_ip"`
|
|
}
|
|
|
|
// ActionDecision is the policy output document for action checks. QualityCeiling
|
|
// is set only when a custom override narrows the input max_playback_quality.
|
|
// Reason is human-readable free text; ReasonCode is the stable machine contract
|
|
// (one of the ReasonCode* constants).
|
|
type ActionDecision struct {
|
|
Allowed bool `json:"allowed"`
|
|
Reason string `json:"reason"`
|
|
ReasonCode string `json:"reason_code"`
|
|
QualityCeiling string `json:"quality_ceiling"`
|
|
}
|