Loading .gitignore +2 −1 Original line number Original line Diff line number Diff line Loading @@ -37,4 +37,5 @@ results helm/capif/*.lock helm/capif/*.lock helm/capif/charts/tempo* helm/capif/charts/tempo* *.bakresults/ *.bak *.bak helm/capif/charts/ocf-helper/templates/ocf-helper-configmap.yaml +4 −0 Original line number Original line Diff line number Diff line Loading @@ -36,5 +36,9 @@ data: "configuration_api": { "configuration_api": { "path": "/configuration", "path": "/configuration", "openapi_file": "configuration/openapi/openapi.yaml" "openapi_file": "configuration/openapi/openapi.yaml" }, "visibility_control": { "path": "/visibility-control", "openapi_file": "visibility_control/openapi/openapi.yaml" } } } } No newline at end of file services/TS29222_CAPIF_API_Invoker_Management_API/requirements.txt +1 −1 Original line number Original line Diff line number Diff line Loading @@ -21,6 +21,6 @@ opentelemetry-api == 1.20.0 opentelemetry-sdk == 1.20.0 opentelemetry-sdk == 1.20.0 flask_executor == 1.0.0 flask_executor == 1.0.0 Flask-APScheduler == 1.13.1 Flask-APScheduler == 1.13.1 werkzeug == 3.0.6 werkzeug == 3.1.4 gunicorn == 23.0.0 gunicorn == 23.0.0 packaging == 24.0 packaging == 24.0 No newline at end of file services/helper/config.yaml +3 −1 Original line number Original line Diff line number Diff line Loading @@ -44,4 +44,6 @@ package_paths: configuration_api: configuration_api: path: /configuration path: /configuration openapi_file: configuration/openapi/openapi.yaml openapi_file: configuration/openapi/openapi.yaml visibility_control: path: /visibility-control openapi_file: visibility_control/openapi/openapi.yaml services/helper/helper_service/openapi_helper_visibility_control.yaml 0 → 100644 +536 −0 Original line number Original line Diff line number Diff line openapi: 3.0.3 info: title: OpenCAPIF Access Control version: 1.0.0 description: | Access-control API to manage visibility rules and evaluate decisions for API discovery and security-context access within OpenCAPIF. This API controls whether APIs are visible to invokers (discovery) and whether invokers are allowed to create a security context to access them. - Rules are global and evaluated with "more specific wins" precedence. - If no rule matches, the decision uses OpenCAPIF's global default (outside this API). - Provider selector is mandatory in rules and must contain at least one selector field. servers: - url: https://capif.example.com/access-control description: Production - url: https://sandbox.capif.example.com/access-control description: Sandbox tags: - name: Rules description: Manage visibility rules - name: Decision description: Evaluate discovery and access decisions paths: /rules: get: tags: [Rules] summary: List rules responses: '200': description: List of rules content: application/json: schema: type: object properties: items: type: array items: $ref: '#/components/schemas/Rule' nextPageToken: type: string required: [items] post: tags: [Rules] summary: Create a rule description: Server generates the ruleId. Provider selector must include at least one field. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/RuleCreateRequest' examples: allow_except_some_invokers: value: providerSelector: userName: "userA" apiProviderId: [ "capif-prov-01", "capif-prov-02" ] apiName: [ "apiName-001" ] apiId: [ "apiId-001" ] aefId: [ "aef-001" ] invokerExceptions: apiInvokerId: [ "invk-123", "invk-999" ] default_access: ALLOW enabled: true responses: '201': description: Rule created content: application/json: schema: $ref: '#/components/schemas/Rule' '400': description: Invalid input content: application/json: schema: { $ref: '#/components/schemas/Error' } /rules/{ruleId}: get: tags: [Rules] summary: Get a rule parameters: - $ref: '#/components/parameters/RuleId' responses: '200': description: Rule content: application/json: schema: $ref: '#/components/schemas/Rule' '404': description: Rule not found content: application/json: schema: { $ref: '#/components/schemas/Error' } patch: tags: [Rules] summary: Update a rule (partial) parameters: - $ref: '#/components/parameters/RuleId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/RulePatchRequest' responses: '200': description: Rule updated content: application/json: schema: $ref: '#/components/schemas/Rule' '400': description: Invalid input content: application/json: schema: { $ref: '#/components/schemas/Error' } '404': description: Rule not found content: application/json: schema: { $ref: '#/components/schemas/Error' } delete: tags: [Rules] summary: Delete a rule parameters: - $ref: '#/components/parameters/RuleId' responses: '204': description: Deleted '404': description: Rule not found content: application/json: schema: { $ref: '#/components/schemas/Error' } /decision/invokers/{apiInvokerId}/discoverable-apis: get: tags: [Decision] summary: Get discoverable APIs filter for an invoker (global scope) description: | Returns a filtered list of APIs for the API Invoker. parameters: - $ref: '#/components/parameters/ApiInvokerId' responses: '200': description: Discover filter content: application/json: schema: $ref: '#/components/schemas/DiscoveredAPIs' '400': description: Invalid input content: application/json: schema: { $ref: '#/components/schemas/Error' } '404': description: Invoker not found (optional behavior) content: application/json: schema: { $ref: '#/components/schemas/Error' } components: parameters: RuleId: in: path name: ruleId required: true schema: type: string description: Server-generated rule identifier ApiInvokerId: in: path name: apiInvokerId required: true schema: type: string description: CAPIF API Invoker identifier schemas: # ---------- Core Rule Schemas ---------- RuleCreateRequest: type: object required: [providerSelector, default_access] properties: providerSelector: $ref: '#/components/schemas/ProviderSelector' invokerExceptions: $ref: '#/components/schemas/InvokerSelector' default_access: type: string enum: [ALLOW, DENY] enabled: type: boolean default: true startsAt: type: string format: date-time endsAt: type: string format: date-time notes: type: string description: | Create a new rule. Provider selector is mandatory and must include at least one field. If both startsAt and endsAt are present, endsAt must be greater than startsAt. RulePatchRequest: type: object properties: providerSelector: $ref: '#/components/schemas/PatchProviderSelector' invokerExceptions: $ref: '#/components/schemas/InvokerSelector' default_access: type: string enum: [ALLOW, DENY] enabled: type: boolean startsAt: type: string format: date-time endsAt: type: string format: date-time notes: type: string description: Partial update. Any omitted field remains unchanged. Rule: type: object properties: ruleId: type: string providerSelector: $ref: '#/components/schemas/ProviderSelector' invokerExceptions: $ref: '#/components/schemas/InvokerSelector' default_access: type: string enum: [ALLOW, DENY] enabled: type: boolean default: true startsAt: type: string format: date-time endsAt: type: string format: date-time notes: type: string updatedAt: type: string format: date-time updatedBy: type: string required: [ruleId, providerSelector, default_access] PatchProviderSelector: type: object description: | Patch Provider-side selector. properties: apiProviderId: type: array items: { type: string } minItems: 0 uniqueItems: true apiName: type: array items: { type: string } minItems: 0 uniqueItems: true apiId: type: array items: { type: string } minItems: 0 uniqueItems: true aefId: type: array items: { type: string } minItems: 0 uniqueItems: true userName: type: string minLength: 1 additionalProperties: false ProviderSelector: type: object description: | Provider-side selector. Arrays apply OR within the field; AND across fields. At least one of these fields must be present. required: - userName properties: userName: type: string minLength: 1 apiProviderId: type: array items: { type: string } minItems: 0 uniqueItems: true apiName: type: array items: { type: string } minItems: 0 uniqueItems: true apiId: type: array items: { type: string } minItems: 0 uniqueItems: true aefId: type: array items: { type: string } minItems: 0 uniqueItems: true additionalProperties: false InvokerSelector: type: object description: Invoker-side selector used for exceptions. Optional; arrays use OR within the field; AND across fields. properties: invokerOnboardedByUser: type: array items: { type: string } minItems: 0 uniqueItems: true apiInvokerId: type: array items: { type: string } minItems: 0 uniqueItems: true additionalProperties: false # ---------- Decision Schemas (3GPP Based) ---------- DiscoveredAPIs: type: object properties: serviceAPIDescriptions: type: array items: $ref: '#/components/schemas/ServiceAPIDescription' minItems: 1 suppFeat: $ref: '#/components/schemas/SupportedFeatures' ServiceAPIDescription: type: object required: [apiName] properties: apiName: { type: string } apiId: { type: string } apiStatus: { $ref: '#/components/schemas/ApiStatus' } aefProfiles: type: array items: { $ref: '#/components/schemas/AefProfile' } minItems: 1 description: { type: string } supportedFeatures: { $ref: '#/components/schemas/SupportedFeatures' } shareableInfo: { $ref: '#/components/schemas/ShareableInformation' } serviceAPICategory: { type: string } apiSuppFeats: { $ref: '#/components/schemas/SupportedFeatures' } pubApiPath: { $ref: '#/components/schemas/PublishedApiPath' } ccfId: { type: string } apiProvName: { type: string } #apiProvName is apiProviderId? AefProfile: type: object required: [aefId, versions] properties: aefId: { type: string } versions: type: array items: { $ref: '#/components/schemas/Version' } minItems: 1 protocol: { $ref: '#/components/schemas/Protocol' } dataFormat: { $ref: '#/components/schemas/DataFormat' } securityMethods: type: array items: { $ref: '#/components/schemas/SecurityMethod' } grantTypes: type: array items: { $ref: '#/components/schemas/OAuthGrantType' } domainName: { type: string } interfaceDescriptions: type: array items: { $ref: '#/components/schemas/InterfaceDescription' } aefLocation: { $ref: '#/components/schemas/AefLocation' } serviceKpis: { $ref: '#/components/schemas/ServiceKpis' } ueIpRange: { $ref: '#/components/schemas/IpAddrRange' } Version: type: object required: [apiVersion] properties: apiVersion: { type: string } expiry: { type: string, format: date-time } resources: type: array items: { $ref: '#/components/schemas/Resource' } custOperations: type: array items: { $ref: '#/components/schemas/CustomOperation' } Resource: type: object required: [commType, resourceName, uri] properties: resourceName: { type: string } commType: { $ref: '#/components/schemas/CommunicationType' } uri: { type: string } custOpName: { type: string } operations: type: array items: { $ref: '#/components/schemas/Operation' } description: { type: string } CustomOperation: type: object required: [commType, custOpName] properties: commType: { $ref: '#/components/schemas/CommunicationType' } custOpName: { type: string } operations: type: array items: { $ref: '#/components/schemas/Operation' } description: { type: string } ApiStatus: type: object required: [aefIds] properties: aefIds: type: array items: { type: string } InterfaceDescription: type: object properties: ipv4Addr: { type: string } ipv6Addr: { type: string } fqdn: { type: string } port: { type: integer } apiPrefix: { type: string } securityMethods: type: array items: { $ref: '#/components/schemas/SecurityMethod' } grantTypes: type: array items: { $ref: '#/components/schemas/OAuthGrantType' } # ---------- Supporting 3GPP Types ---------- SupportedFeatures: type: string pattern: "^[A-Fa-f0-9]*$" CommunicationType: type: string enum: [REQUEST_RESPONSE, SUBSCRIBE_NOTIFY] Protocol: type: string enum: [HTTP_1_1, HTTP_2, MQTT, WEBSOCKET] DataFormat: type: string enum: [JSON, XML, PROTOBUF3] Operation: type: string enum: [GET, POST, PUT, PATCH, DELETE] SecurityMethod: type: string enum: [PSK, PKI, OAUTH] OAuthGrantType: type: string enum: [CLIENT_CREDENTIALS, AUTHORIZATION_CODE, AUTHORIZATION_CODE_WITH_PKCE] ShareableInformation: type: object required: [isShareable] properties: isShareable: { type: boolean } capifProvDoms: type: array items: { type: string } PublishedApiPath: type: object properties: ccfIds: type: array items: { type: string } AefLocation: type: object properties: dcId: { type: string } # Simplified for brevity, you can add GeographicArea/CivicAddress if needed ServiceKpis: type: object properties: maxReqRate: { type: integer } maxRestime: { type: integer } availability: { type: integer } avalComp: { type: string } avalMem: { type: string } avalStor: { type: string } IpAddrRange: type: object properties: ueIpv4AddrRanges: type: array items: type: object properties: start: { type: string } end: { type: string } # ---------- Errors ---------- Error: type: object required: [code, message] properties: code: { type: string } message: { type: string } details: type: object additionalProperties: true No newline at end of file Loading
.gitignore +2 −1 Original line number Original line Diff line number Diff line Loading @@ -37,4 +37,5 @@ results helm/capif/*.lock helm/capif/*.lock helm/capif/charts/tempo* helm/capif/charts/tempo* *.bakresults/ *.bak *.bak
helm/capif/charts/ocf-helper/templates/ocf-helper-configmap.yaml +4 −0 Original line number Original line Diff line number Diff line Loading @@ -36,5 +36,9 @@ data: "configuration_api": { "configuration_api": { "path": "/configuration", "path": "/configuration", "openapi_file": "configuration/openapi/openapi.yaml" "openapi_file": "configuration/openapi/openapi.yaml" }, "visibility_control": { "path": "/visibility-control", "openapi_file": "visibility_control/openapi/openapi.yaml" } } } } No newline at end of file
services/TS29222_CAPIF_API_Invoker_Management_API/requirements.txt +1 −1 Original line number Original line Diff line number Diff line Loading @@ -21,6 +21,6 @@ opentelemetry-api == 1.20.0 opentelemetry-sdk == 1.20.0 opentelemetry-sdk == 1.20.0 flask_executor == 1.0.0 flask_executor == 1.0.0 Flask-APScheduler == 1.13.1 Flask-APScheduler == 1.13.1 werkzeug == 3.0.6 werkzeug == 3.1.4 gunicorn == 23.0.0 gunicorn == 23.0.0 packaging == 24.0 packaging == 24.0 No newline at end of file
services/helper/config.yaml +3 −1 Original line number Original line Diff line number Diff line Loading @@ -44,4 +44,6 @@ package_paths: configuration_api: configuration_api: path: /configuration path: /configuration openapi_file: configuration/openapi/openapi.yaml openapi_file: configuration/openapi/openapi.yaml visibility_control: path: /visibility-control openapi_file: visibility_control/openapi/openapi.yaml
services/helper/helper_service/openapi_helper_visibility_control.yaml 0 → 100644 +536 −0 Original line number Original line Diff line number Diff line openapi: 3.0.3 info: title: OpenCAPIF Access Control version: 1.0.0 description: | Access-control API to manage visibility rules and evaluate decisions for API discovery and security-context access within OpenCAPIF. This API controls whether APIs are visible to invokers (discovery) and whether invokers are allowed to create a security context to access them. - Rules are global and evaluated with "more specific wins" precedence. - If no rule matches, the decision uses OpenCAPIF's global default (outside this API). - Provider selector is mandatory in rules and must contain at least one selector field. servers: - url: https://capif.example.com/access-control description: Production - url: https://sandbox.capif.example.com/access-control description: Sandbox tags: - name: Rules description: Manage visibility rules - name: Decision description: Evaluate discovery and access decisions paths: /rules: get: tags: [Rules] summary: List rules responses: '200': description: List of rules content: application/json: schema: type: object properties: items: type: array items: $ref: '#/components/schemas/Rule' nextPageToken: type: string required: [items] post: tags: [Rules] summary: Create a rule description: Server generates the ruleId. Provider selector must include at least one field. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/RuleCreateRequest' examples: allow_except_some_invokers: value: providerSelector: userName: "userA" apiProviderId: [ "capif-prov-01", "capif-prov-02" ] apiName: [ "apiName-001" ] apiId: [ "apiId-001" ] aefId: [ "aef-001" ] invokerExceptions: apiInvokerId: [ "invk-123", "invk-999" ] default_access: ALLOW enabled: true responses: '201': description: Rule created content: application/json: schema: $ref: '#/components/schemas/Rule' '400': description: Invalid input content: application/json: schema: { $ref: '#/components/schemas/Error' } /rules/{ruleId}: get: tags: [Rules] summary: Get a rule parameters: - $ref: '#/components/parameters/RuleId' responses: '200': description: Rule content: application/json: schema: $ref: '#/components/schemas/Rule' '404': description: Rule not found content: application/json: schema: { $ref: '#/components/schemas/Error' } patch: tags: [Rules] summary: Update a rule (partial) parameters: - $ref: '#/components/parameters/RuleId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/RulePatchRequest' responses: '200': description: Rule updated content: application/json: schema: $ref: '#/components/schemas/Rule' '400': description: Invalid input content: application/json: schema: { $ref: '#/components/schemas/Error' } '404': description: Rule not found content: application/json: schema: { $ref: '#/components/schemas/Error' } delete: tags: [Rules] summary: Delete a rule parameters: - $ref: '#/components/parameters/RuleId' responses: '204': description: Deleted '404': description: Rule not found content: application/json: schema: { $ref: '#/components/schemas/Error' } /decision/invokers/{apiInvokerId}/discoverable-apis: get: tags: [Decision] summary: Get discoverable APIs filter for an invoker (global scope) description: | Returns a filtered list of APIs for the API Invoker. parameters: - $ref: '#/components/parameters/ApiInvokerId' responses: '200': description: Discover filter content: application/json: schema: $ref: '#/components/schemas/DiscoveredAPIs' '400': description: Invalid input content: application/json: schema: { $ref: '#/components/schemas/Error' } '404': description: Invoker not found (optional behavior) content: application/json: schema: { $ref: '#/components/schemas/Error' } components: parameters: RuleId: in: path name: ruleId required: true schema: type: string description: Server-generated rule identifier ApiInvokerId: in: path name: apiInvokerId required: true schema: type: string description: CAPIF API Invoker identifier schemas: # ---------- Core Rule Schemas ---------- RuleCreateRequest: type: object required: [providerSelector, default_access] properties: providerSelector: $ref: '#/components/schemas/ProviderSelector' invokerExceptions: $ref: '#/components/schemas/InvokerSelector' default_access: type: string enum: [ALLOW, DENY] enabled: type: boolean default: true startsAt: type: string format: date-time endsAt: type: string format: date-time notes: type: string description: | Create a new rule. Provider selector is mandatory and must include at least one field. If both startsAt and endsAt are present, endsAt must be greater than startsAt. RulePatchRequest: type: object properties: providerSelector: $ref: '#/components/schemas/PatchProviderSelector' invokerExceptions: $ref: '#/components/schemas/InvokerSelector' default_access: type: string enum: [ALLOW, DENY] enabled: type: boolean startsAt: type: string format: date-time endsAt: type: string format: date-time notes: type: string description: Partial update. Any omitted field remains unchanged. Rule: type: object properties: ruleId: type: string providerSelector: $ref: '#/components/schemas/ProviderSelector' invokerExceptions: $ref: '#/components/schemas/InvokerSelector' default_access: type: string enum: [ALLOW, DENY] enabled: type: boolean default: true startsAt: type: string format: date-time endsAt: type: string format: date-time notes: type: string updatedAt: type: string format: date-time updatedBy: type: string required: [ruleId, providerSelector, default_access] PatchProviderSelector: type: object description: | Patch Provider-side selector. properties: apiProviderId: type: array items: { type: string } minItems: 0 uniqueItems: true apiName: type: array items: { type: string } minItems: 0 uniqueItems: true apiId: type: array items: { type: string } minItems: 0 uniqueItems: true aefId: type: array items: { type: string } minItems: 0 uniqueItems: true userName: type: string minLength: 1 additionalProperties: false ProviderSelector: type: object description: | Provider-side selector. Arrays apply OR within the field; AND across fields. At least one of these fields must be present. required: - userName properties: userName: type: string minLength: 1 apiProviderId: type: array items: { type: string } minItems: 0 uniqueItems: true apiName: type: array items: { type: string } minItems: 0 uniqueItems: true apiId: type: array items: { type: string } minItems: 0 uniqueItems: true aefId: type: array items: { type: string } minItems: 0 uniqueItems: true additionalProperties: false InvokerSelector: type: object description: Invoker-side selector used for exceptions. Optional; arrays use OR within the field; AND across fields. properties: invokerOnboardedByUser: type: array items: { type: string } minItems: 0 uniqueItems: true apiInvokerId: type: array items: { type: string } minItems: 0 uniqueItems: true additionalProperties: false # ---------- Decision Schemas (3GPP Based) ---------- DiscoveredAPIs: type: object properties: serviceAPIDescriptions: type: array items: $ref: '#/components/schemas/ServiceAPIDescription' minItems: 1 suppFeat: $ref: '#/components/schemas/SupportedFeatures' ServiceAPIDescription: type: object required: [apiName] properties: apiName: { type: string } apiId: { type: string } apiStatus: { $ref: '#/components/schemas/ApiStatus' } aefProfiles: type: array items: { $ref: '#/components/schemas/AefProfile' } minItems: 1 description: { type: string } supportedFeatures: { $ref: '#/components/schemas/SupportedFeatures' } shareableInfo: { $ref: '#/components/schemas/ShareableInformation' } serviceAPICategory: { type: string } apiSuppFeats: { $ref: '#/components/schemas/SupportedFeatures' } pubApiPath: { $ref: '#/components/schemas/PublishedApiPath' } ccfId: { type: string } apiProvName: { type: string } #apiProvName is apiProviderId? AefProfile: type: object required: [aefId, versions] properties: aefId: { type: string } versions: type: array items: { $ref: '#/components/schemas/Version' } minItems: 1 protocol: { $ref: '#/components/schemas/Protocol' } dataFormat: { $ref: '#/components/schemas/DataFormat' } securityMethods: type: array items: { $ref: '#/components/schemas/SecurityMethod' } grantTypes: type: array items: { $ref: '#/components/schemas/OAuthGrantType' } domainName: { type: string } interfaceDescriptions: type: array items: { $ref: '#/components/schemas/InterfaceDescription' } aefLocation: { $ref: '#/components/schemas/AefLocation' } serviceKpis: { $ref: '#/components/schemas/ServiceKpis' } ueIpRange: { $ref: '#/components/schemas/IpAddrRange' } Version: type: object required: [apiVersion] properties: apiVersion: { type: string } expiry: { type: string, format: date-time } resources: type: array items: { $ref: '#/components/schemas/Resource' } custOperations: type: array items: { $ref: '#/components/schemas/CustomOperation' } Resource: type: object required: [commType, resourceName, uri] properties: resourceName: { type: string } commType: { $ref: '#/components/schemas/CommunicationType' } uri: { type: string } custOpName: { type: string } operations: type: array items: { $ref: '#/components/schemas/Operation' } description: { type: string } CustomOperation: type: object required: [commType, custOpName] properties: commType: { $ref: '#/components/schemas/CommunicationType' } custOpName: { type: string } operations: type: array items: { $ref: '#/components/schemas/Operation' } description: { type: string } ApiStatus: type: object required: [aefIds] properties: aefIds: type: array items: { type: string } InterfaceDescription: type: object properties: ipv4Addr: { type: string } ipv6Addr: { type: string } fqdn: { type: string } port: { type: integer } apiPrefix: { type: string } securityMethods: type: array items: { $ref: '#/components/schemas/SecurityMethod' } grantTypes: type: array items: { $ref: '#/components/schemas/OAuthGrantType' } # ---------- Supporting 3GPP Types ---------- SupportedFeatures: type: string pattern: "^[A-Fa-f0-9]*$" CommunicationType: type: string enum: [REQUEST_RESPONSE, SUBSCRIBE_NOTIFY] Protocol: type: string enum: [HTTP_1_1, HTTP_2, MQTT, WEBSOCKET] DataFormat: type: string enum: [JSON, XML, PROTOBUF3] Operation: type: string enum: [GET, POST, PUT, PATCH, DELETE] SecurityMethod: type: string enum: [PSK, PKI, OAUTH] OAuthGrantType: type: string enum: [CLIENT_CREDENTIALS, AUTHORIZATION_CODE, AUTHORIZATION_CODE_WITH_PKCE] ShareableInformation: type: object required: [isShareable] properties: isShareable: { type: boolean } capifProvDoms: type: array items: { type: string } PublishedApiPath: type: object properties: ccfIds: type: array items: { type: string } AefLocation: type: object properties: dcId: { type: string } # Simplified for brevity, you can add GeographicArea/CivicAddress if needed ServiceKpis: type: object properties: maxReqRate: { type: integer } maxRestime: { type: integer } availability: { type: integer } avalComp: { type: string } avalMem: { type: string } avalStor: { type: string } IpAddrRange: type: object properties: ueIpv4AddrRanges: type: array items: type: object properties: start: { type: string } end: { type: string } # ---------- Errors ---------- Error: type: object required: [code, message] properties: code: { type: string } message: { type: string } details: type: object additionalProperties: true No newline at end of file