Files
silo-server/internal/policy/input.go
28c6ddc237 feat(playback): add per-user transcoding controls (#375)
* 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>
2026-07-10 22:30:04 -04:00

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"`
}