]> git.ipfire.org Git - thirdparty/cups.git/commitdiff
Add support for cached authentication on a per-job basis. The
authormike <mike@7a7537e8-13f0-0310-91df-b6672ffda945>
Mon, 26 Sep 2005 20:59:15 +0000 (20:59 +0000)
committermike <mike@7a7537e8-13f0-0310-91df-b6672ffda945>
Mon, 26 Sep 2005 20:59:15 +0000 (20:59 +0000)
information is stored in a file that is readable only by the root
user.  The username and password are Base-64 encoded, each on a
separate line, followed by random number (up to 1024) of newlines
to limit the amount of information that is exposed.

Because of the potential for exposing of authentication
information, this functionality is only enabled when running
cupsd as root.

This caching only works for the Basic and BasicDigest
authentication types.  Digest authentication cannot be cached
this way, and in the future Kerberos authentication may make all
of this obsolete.

Authentication information is saved whenever an authenticated
Print-Job, Create-Job, or CUPS-Authenticate-Job operation is
performed.

This information is deleted after a job is completed or canceled,
so reprints may require subsequent re-authentication.

backend/ipp.c:
    - main(): If no authentication information is in the device
      URI, try reading it from the auth cache file.

conf/cupsd.conf.in:
templates/edit-config.tmpl.in:
    - Add CUPS-Authenticate-Job policy.

cups/ipp.h:
    - Add CUPS_AUTHENTICATE_JOB operation.

cups/ipp-support.c:
    - Add CUPS-Authenticate-Job operation string.

scheduler/conf.c:
    - ReadConfiguration(): Add CUPS_AUTHENTICATE_JOB operation to
      the default policy.

scheduler/env.c:
    - cupsInitEnv(): Add CUPS_REQUESTROOT environment variable.

scheduler/ipp.c:
    - ProcessIPPRequest(): Add CUPS_AUTHENTICATE_JOB operation.
    - authenticate_job(): Added.
    - create_job(), print_job(): Call save_auth_info() when the
      request is authenticated and require authentication if the
      printer-type CUPS_PRINTER_AUTHENTICATED bit is set.
    - save_auth_info(): Added.

scheduler/job.c:
    - CancelJob(): Remove authentication cache file.
    - FinishJob(): Add job-stopped event when authentication is
      required.

scheduler/printers.c:
    - CreateCommonData(): Update operations-supported list.
    - SetPrinterAttrs(): Use both Location and Policy limits to
      mark whether authentication is required.

git-svn-id: svn+ssh://src.apple.com/svn/cups/cups.org/trunk@4704 7a7537e8-13f0-0310-91df-b6672ffda945

backend/ipp.c
conf/cupsd.conf.in
cups/ipp-support.c
cups/ipp.h
scheduler/conf.c
scheduler/env.c
scheduler/ipp.c
scheduler/job.c
scheduler/printers.c
templates/edit-config.tmpl.in

index aea0ba19831f5a61eca1fac47bbda316df5f9c26..278db2106c1f64b250f49798bb1968609ac1e6ec 100644 (file)
@@ -395,11 +395,73 @@ main(int  argc,                           /* I - Number of command-line arguments (6 or 7) */
 
   if (username[0])
   {
+   /*
+    * Use authenticaion information in the device URI...
+    */
+
     if ((password = strchr(username, ':')) != NULL)
       *password++ = '\0';
 
     cupsSetUser(username);
   }
+  else if (!getuid())
+  {
+   /*
+    * Try loading authentication information from the a##### file.
+    */
+
+    const char *request_root;          /* CUPS_REQUESTROOT env var */
+    char       afilename[1024],        /* a##### filename */
+               aline[1024];            /* Line from file */
+    FILE       *fp;                    /* File pointer */
+
+
+    if ((request_root = getenv("CUPS_REQUESTROOT")) != NULL)
+    {
+     /*
+      * Try opening authentication cache file...
+      */
+
+      snprintf(afilename, sizeof(afilename), "%s/a%05d", request_root,
+               atoi(argv[1]));
+      if ((fp = fopen(afilename, "r")) != NULL)
+      {
+       /*
+        * Read username...
+       */
+
+        if (fgets(aline, sizeof(aline), fp))
+       {
+        /*
+         * Decode username...
+         */
+
+          i = sizeof(username);
+         httpDecode64_2(username, &i, aline);
+
+         /*
+         * Read password...
+         */
+
+         if (fgets(aline, sizeof(aline), fp))
+         {
+          /*
+           * Decode password...
+           */
+
+           i = sizeof(password);
+           httpDecode64_2(password, &i, aline);
+         }
+       }
+
+       /*
+        * Close the file...
+       */
+
+        fclose(fp);
+      }
+    }
+  }
 
  /*
   * Try connecting to the remote server...
index 839d34ffab91e89e1c3e968e66d3376efcb22fcb..029637b046afe3ed7b9ec0393904d3a27c20f415 100644 (file)
@@ -63,8 +63,8 @@ DefaultAuthType Basic
     Order deny,allow
   </Limit>
 
-  # Only the owner or an administrator can cancel a job...
-  <Limit Cancel-Job>
+  # Only the owner or an administrator can cancel or authenticate a job...
+  <Limit Cancel-Job CUPS-Authenticate-Job>
     Require user @OWNER @SYSTEM
     Order deny,allow
   </Limit>
index fb2cb2e5d9b1ca18648378370971ca600576416c..31ae7d2189545cc9a78187ffcddce51e4882642a 100644 (file)
@@ -155,7 +155,8 @@ static char * const ipp_std_ops[] =
                  "CUPS-Set-Default",
                  "CUPS-Get-Devices",
                  "CUPS-Get-PPDs",
-                 "CUPS-Move-Job"
+                 "CUPS-Move-Job",
+                 "CUPS-Authenticate-Job"
                };
 
 
@@ -239,7 +240,7 @@ ippOpString(ipp_op_t op)            /* I - Operation ID */
     return (ipp_std_ops[op]);
   else if (op == IPP_PRIVATE)
     return ("windows-ext");
-  else if (op >= CUPS_GET_DEFAULT && op <= CUPS_MOVE_JOB)
+  else if (op >= CUPS_GET_DEFAULT && op <= CUPS_AUTHENTICATE_JOB)
     return (ipp_cups_ops[op - CUPS_GET_DEFAULT]);
 
  /*
index b55dd9ed51c68f659b8172286d5374a8ff0fc288..6f81e10db4c6a26abec7ea41f54a7fbf3208031e 100644 (file)
@@ -252,7 +252,8 @@ typedef enum                        /**** IPP operations... ****/
   CUPS_SET_DEFAULT,
   CUPS_GET_DEVICES,
   CUPS_GET_PPDS,
-  CUPS_MOVE_JOB
+  CUPS_MOVE_JOB,
+  CUPS_AUTHENTICATE_JOB
 } ipp_op_t;
 
 typedef enum                   /**** IPP status codes... ****/
index af139e5519855b2845da2880de70da1cc1e0f7ed..a2443a071bbb3e303eac9c7f99ca3c954b7f959a 100644 (file)
@@ -801,7 +801,8 @@ ReadConfiguration(void)
                         "Set-Job-Attributes Create-Job-Subscription "
                         "Renew-Subscription Cancel-Subscription "
                         "Get-Notifications Reprocess-Job Cancel-Current-Job "
-                        "Suspend-Current-Job Resume-Job CUPS-Move-Job>");
+                        "Suspend-Current-Job Resume-Job CUPS-Move-Job "
+                        "CUPS-Authenticate-Job>");
       LogMessage(L_INFO, "Order Deny,Allow");
 
       po = cupsdAddPolicyOp(p, NULL, IPP_SEND_DOCUMENT);
@@ -828,6 +829,7 @@ ReadConfiguration(void)
       cupsdAddPolicyOp(p, po, IPP_SUSPEND_CURRENT_JOB);
       cupsdAddPolicyOp(p, po, IPP_RESUME_JOB);
       cupsdAddPolicyOp(p, po, CUPS_MOVE_JOB);
+      cupsdAddPolicyOp(p, po, CUPS_AUTHENTICATE_JOB);
 
       LogMessage(L_INFO, "</Limit>");
 
index 7da107c86bb18f35c653263349575f02f12b3cc5..62a38f38f22f1dccf0d069b242aefae5fed1afef 100644 (file)
@@ -94,6 +94,7 @@ cupsdInitEnv(void)
   cupsdSetEnv("CUPS_DATADIR", DataDir);
   cupsdSetEnv("CUPS_DOCROOT", DocumentRoot);
   cupsdSetEnv("CUPS_FONTPATH", FontPath);
+  cupsdSetEnv("CUPS_REQUESTROOT", RequestRoot);
   cupsdSetEnv("CUPS_SERVERBIN", ServerBin);
   cupsdSetEnv("CUPS_SERVERROOT", ServerRoot);
   cupsdSetEnv("CUPS_STATEDIR", StateDir);
index 4acd755d300e1fb47fef9bf4d7736231b3443c5c..c367649a08904d60c828fa7bc84ef9d0f19a048a 100644 (file)
@@ -34,6 +34,7 @@
  *   add_printer_state_reasons() - Add the "printer-state-reasons" attribute
  *                                 based upon the printer state...
  *   add_queued_job_count()      - Add the "queued-job-count" attribute for
+ *   authenticate_job()          - Set job authentication info.
  *   cancel_all_jobs()           - Cancel all print jobs.
  *   cancel_job()                - Cancel a print job.
  *   cancel_subscription()       - Cancel a subscription.
@@ -70,6 +71,7 @@
  *   reject_jobs()               - Reject print jobs to a printer.
  *   release_job()               - Release a held print job.
  *   restart_job()               - Restart an old print job.
+ *   save_auth_info()            - Save authentication information for a job.
  *   send_document()             - Send a file to a printer or class.
  *   send_ipp_error()            - Send an error status back to the IPP client.
  *   set_default()               - Set the default destination...
@@ -118,6 +120,7 @@ static void add_job_subscriptions(client_t *con, job_t *job);
 static void    add_printer(client_t *con, ipp_attribute_t *uri);
 static void    add_printer_state_reasons(client_t *con, printer_t *p);
 static void    add_queued_job_count(client_t *con, printer_t *p);
+static void    authenticate_job(client_t *con, ipp_attribute_t *uri);
 static void    cancel_all_jobs(client_t *con, ipp_attribute_t *uri);
 static void    cancel_job(client_t *con, ipp_attribute_t *uri);
 static void    cancel_subscription(client_t *con, int id);
@@ -154,6 +157,7 @@ static void reject_jobs(client_t *con, ipp_attribute_t *uri);
 static void    release_job(client_t *con, ipp_attribute_t *uri);
 static void    renew_subscription(client_t *con, int sub_id);
 static void    restart_job(client_t *con, ipp_attribute_t *uri);
+static void    save_auth_info(client_t *con, int job_id);
 static void    send_document(client_t *con, ipp_attribute_t *uri);
 static void    send_ipp_error(client_t *con, ipp_status_t status);
 static void    set_default(client_t *con, ipp_attribute_t *uri);
@@ -513,6 +517,10 @@ ProcessIPPRequest(client_t *con)   /* I - Client connection */
               move_job(con, uri);
               break;
 
+         case CUPS_AUTHENTICATE_JOB :
+              authenticate_job(con, uri);
+              break;
+
           case IPP_CREATE_PRINTER_SUBSCRIPTION :
          case IPP_CREATE_JOB_SUBSCRIPTION :
              create_subscription(con, uri);
@@ -1963,6 +1971,161 @@ add_queued_job_count(client_t  *con,    /* I - Client connection */
 }
 
 
+/*
+ * 'authenticate_job()' - Set job authentication info.
+ */
+
+static void
+authenticate_job(client_t        *con, /* I - Client connection */
+                ipp_attribute_t *uri)  /* I - Job URI */
+{
+  ipp_attribute_t      *attr;          /* Job-id attribute */
+  int                  jobid;          /* Job ID */
+  job_t                        *job;           /* Current job */
+  char                 method[HTTP_MAX_URI],
+                                       /* Method portion of URI */
+                       username[HTTP_MAX_URI],
+                                       /* Username portion of URI */
+                       host[HTTP_MAX_URI],
+                                       /* Host portion of URI */
+                       resource[HTTP_MAX_URI];
+                                       /* Resource portion of URI */
+  int                  port;           /* Port portion of URI */
+
+
+  LogMessage(L_DEBUG2, "authenticate_job(%p[%d], %s)\n", con, con->http.fd,
+             uri->values[0].string.text);
+
+ /*
+  * Start with "everything is OK" status...
+  */
+
+  con->response->request.status.status_code = IPP_OK;
+
+ /*
+  * See if we have a job URI or a printer URI...
+  */
+
+  if (!strcmp(uri->name, "printer-uri"))
+  {
+   /*
+    * Got a printer URI; see if we also have a job-id attribute...
+    */
+
+    if ((attr = ippFindAttribute(con->request, "job-id", IPP_TAG_INTEGER)) == NULL)
+    {
+      LogMessage(L_ERROR, "authenticate_job: got a printer-uri attribute but no job-id!");
+      send_ipp_error(con, IPP_BAD_REQUEST);
+      return;
+    }
+
+    jobid = attr->values[0].integer;
+  }
+  else
+  {
+   /*
+    * Got a job URI; parse it to get the job ID...
+    */
+
+    httpSeparate(uri->values[0].string.text, method, username, host, &port, resource);
+    if (strncmp(resource, "/jobs/", 6))
+    {
+     /*
+      * Not a valid URI!
+      */
+
+      LogMessage(L_ERROR, "authenticate_job: bad job-uri attribute \'%s\'!\n",
+                 uri->values[0].string.text);
+      send_ipp_error(con, IPP_BAD_REQUEST);
+      return;
+    }
+
+    jobid = atoi(resource + 6);
+  }
+
+ /*
+  * See if the job exists...
+  */
+
+  if ((job = FindJob(jobid)) == NULL)
+  {
+   /*
+    * Nope - return a "not found" error...
+    */
+
+    LogMessage(L_ERROR, "authenticate_job: job #%d doesn't exist!", jobid);
+    send_ipp_error(con, IPP_NOT_FOUND);
+    return;
+  }
+
+ /*
+  * See if the job has been completed...
+  */
+
+  if (job->state->values[0].integer != IPP_JOB_HELD)
+  {
+   /*
+    * Return a "not-possible" error...
+    */
+
+    LogMessage(L_ERROR, "authenticate_job: job #%d is not held for authentication!", jobid);
+    send_ipp_error(con, IPP_NOT_POSSIBLE);
+    return;
+  }
+
+ /*
+  * See if we have already authenticated...
+  */
+
+  if (!con->username[0])
+  {
+    send_ipp_error(con, IPP_NOT_AUTHORIZED);
+    return;
+  }
+
+ /*
+  * See if the job is owned by the requesting user...
+  */
+
+  if (!validate_user(job, con, job->username, username, sizeof(username)))
+  {
+    LogMessage(L_ERROR, "authenticate_job: \"%s\" not authorized to authenticate job id %d owned by \"%s\"!",
+               username, jobid, job->username);
+    send_ipp_error(con, IPP_FORBIDDEN);
+    return;
+  }
+
+ /*
+  * Save the authentication information for this job...
+  */
+
+  save_auth_info(con, job->id);
+
+ /*
+  * Reset the job-hold-until value to "no-hold"...
+  */
+
+  if ((attr = ippFindAttribute(job->attrs, "job-hold-until", IPP_TAG_KEYWORD)) == NULL)
+    attr = ippFindAttribute(job->attrs, "job-hold-until", IPP_TAG_NAME);
+
+  if (attr != NULL)
+  {
+    attr->value_tag = IPP_TAG_KEYWORD;
+    SetString(&(attr->values[0].string.text), "no-hold");
+  }
+
+ /*
+  * Release the job and return...
+  */
+
+  ReleaseJob(jobid);
+
+  LogMessage(L_INFO, "Job %d was authenticated by \'%s\'.", jobid,
+             con->username);
+}
+
+
 /*
  * 'cancel_all_jobs()' - Cancel all print jobs.
  */
@@ -3427,7 +3590,8 @@ create_job(client_t        *con,  /* I - Client connection */
   * Check policy...
   */
 
-  if (!cupsdCheckPolicy(printer->op_policy_ptr, con, NULL))
+  if (!cupsdCheckPolicy(printer->op_policy_ptr, con, NULL) ||
+      ((printer->type & CUPS_PRINTER_AUTHENTICATED) && !con->username[0]))
   {
     LogMessage(L_ERROR, "create_job: not authorized!");
     send_ipp_error(con, IPP_NOT_AUTHORIZED);
@@ -3531,7 +3695,10 @@ create_job(client_t        *con, /* I - Client connection */
   attr = ippFindAttribute(job->attrs, "requesting-user-name", IPP_TAG_NAME);
 
   if (con->username[0])
+  {
     SetString(&job->username, con->username);
+    save_auth_info(con, job->id);
+  }
   else if (attr != NULL)
   {
     LogMessage(L_DEBUG, "create_job: requesting-user-name = \'%s\'",
@@ -5609,8 +5776,7 @@ print_job(client_t        *con,           /* I - Client connection */
     strcpy(type, "octet-stream");
   }
 
-  if (strcmp(super, "application") == 0 &&
-      strcmp(type, "octet-stream") == 0)
+  if (!strcmp(super, "application") && !strcmp(type, "octet-stream"))
   {
    /*
     * Auto-type the file...
@@ -5665,8 +5831,8 @@ print_job(client_t        *con,           /* I - Client connection */
   * Read any embedded job ticket info from PS files...
   */
 
-  if (strcasecmp(filetype->super, "application") == 0 &&
-      strcasecmp(filetype->type, "postscript") == 0)
+  if (!strcasecmp(filetype->super, "application") &&
+      !strcasecmp(filetype->type, "postscript"))
     read_ps_job_ticket(con);
 
  /*
@@ -5703,7 +5869,8 @@ print_job(client_t        *con,           /* I - Client connection */
   * Check policy...
   */
 
-  if (!cupsdCheckPolicy(printer->op_policy_ptr, con, NULL))
+  if (!cupsdCheckPolicy(printer->op_policy_ptr, con, NULL) ||
+      ((printer->type & CUPS_PRINTER_AUTHENTICATED) && !con->username[0]))
   {
     LogMessage(L_ERROR, "print_job: not authorized!");
     send_ipp_error(con, IPP_NOT_AUTHORIZED);
@@ -5778,7 +5945,10 @@ print_job(client_t        *con,          /* I - Client connection */
   attr = ippFindAttribute(job->attrs, "requesting-user-name", IPP_TAG_NAME);
 
   if (con->username[0])
+  {
     SetString(&job->username, con->username);
+    save_auth_info(con, job->id);
+  }
   else if (attr != NULL)
   {
     LogMessage(L_DEBUG, "print_job: requesting-user-name = \'%s\'",
@@ -6732,6 +6902,90 @@ restart_job(client_t        *con,        /* I - Client connection */
 }
 
 
+/*
+ * 'save_auth_info()' - Save authentication information for a job.
+ */
+
+static void
+save_auth_info(client_t *con,          /* I - Client connection */
+               int      job_id)                /* I - Job ID */
+{
+  int          i;                      /* Looping var */
+  char         filename[1024];         /* Job authentication filename */
+  cups_file_t  *fp;                    /* Job authentication file */
+  char         line[1024];             /* Line for file */
+
+
+ /*
+  * This function saves the in-memory authentication information for
+  * a job so that it can be used to authenticate with a remote host.
+  * The information is stored in a file that is readable only by the
+  * root user.  The username and password are Base-64 encoded, each
+  * on a separate line, followed by random number (up to 1024) of
+  * newlines to limit the amount of information that is exposed.
+  *
+  * Because of the potential for exposing of authentication information,
+  * this functionality is only enabled when running cupsd as root.
+  *
+  * This caching only works for the Basic and BasicDigest authentication
+  * types.  Digest authentication cannot be cached this way, and in
+  * the future Kerberos authentication may make all of this obsolete.
+  *
+  * Authentication information is saved whenever an authenticated
+  * Print-Job, Create-Job, or CUPS-Authenticate-Job operation is
+  * performed.
+  *
+  * This information is deleted after a job is completed or canceled,
+  * so reprints may require subsequent re-authentication.
+  */
+
+  if (RunUser)
+    return;
+
+ /*
+  * Create the authentication file and change permissions...
+  */
+
+  snprintf(filename, sizeof(filename), "%s/a%05d", RequestRoot, job_id);
+  if ((fp = cupsFileOpen(filename, "w")) == NULL)
+  {
+    LogMessage(L_ERROR, "Unable to save authentication info to \"%s\" - %s",
+               filename, strerror(errno));
+    return;
+  }
+
+  fchown(cupsFileNumber(fp), 0, 0);
+  fchmod(cupsFileNumber(fp), 0400);
+
+ /*
+  * Write the authenticated username...
+  */
+
+  httpEncode64_2(line, sizeof(line), con->username, strlen(con->username));
+  cupsFilePrintf(fp, "%s\n", line);
+
+ /*
+  * Write the authenticated password...
+  */
+
+  httpEncode64_2(line, sizeof(line), con->password, strlen(con->password));
+  cupsFilePrintf(fp, "%s\n", line);
+
+ /*
+  * Write a random number of newlines to the end of the file...
+  */
+
+  for (i = (rand() % 1024); i >= 0; i --)
+    cupsFilePutChar(fp, '\n');
+
+ /*
+  * Close the file and return...
+  */
+
+  cupsFileClose(fp);
+}
+
+
 /*
  * 'send_document()' - Send a file to a printer or class.
  */
index 45c9e916bf93fa6c1245a15c79bd739c0e82d5fe..0d91dae9c47adc559c3c96b8bb7e59cb7a4299cc 100644 (file)
@@ -148,7 +148,7 @@ CancelJob(int id,                   /* I - Job to cancel */
     if (current->id == id)
     {
      /*
-      * Stop any processes that are working on the current...
+      * Stop any processes that are working on the current job...
       */
 
       DEBUG_puts("CancelJob: found job in list.");
@@ -162,6 +162,14 @@ CancelJob(int id,                  /* I - Job to cancel */
 
       cupsdExpireSubscriptions(NULL, current);
 
+     /*
+      * Remove any authentication data...
+      */
+
+      snprintf(filename, sizeof(filename), "%s/a%05d", RequestRoot,
+              current->id);
+      unlink(filename);
+
      /*
       * Remove the print file for good if we aren't preserving jobs or
       * files...
@@ -528,6 +536,9 @@ FinishJob(job_t *job)                       /* I - Job */
          /**** TODO ****/
          /* cupsdSetJobStateReasons(job->id, "authentication-required"); */
          SaveJob(job->id);
+
+         cupsdAddEvent(CUPSD_EVENT_JOB_STOPPED, printer, job,
+                       "Authentication is required for job %d.", job->id);
           break;
     }
 
index cd78705d984a95508439491e82a70985c79f4fa3..41584ce7f661caf2fe844f0850f54e1d387d14ac 100644 (file)
@@ -374,8 +374,11 @@ CreateCommonData(void)
                  CUPS_DELETE_CLASS,
                  CUPS_ACCEPT_JOBS,
                  CUPS_REJECT_JOBS,
+                 CUPS_SET_DEFAULT,
                  CUPS_GET_DEVICES,
                  CUPS_GET_PPDS,
+                 CUPS_MOVE_JOB,
+                 CUPS_AUTHENTICATE_JOB,
                  IPP_RESTART_JOB
                };
   static const char * const charsets[] =/* charset-supported values */
@@ -1363,13 +1366,23 @@ SetPrinterAttrs(printer_t *p)           /* I - Printer to setup */
     else
       snprintf(resource, sizeof(resource), "/printers/%s", p->name);
 
-    if ((auth = FindBest(resource, HTTP_POST)) != NULL)
+    if ((auth = FindBest(resource, HTTP_POST)) == NULL)
+      auth = cupsdFindPolicyOp(p->op_policy_ptr, IPP_PRINT_JOB);
+
+    if (auth)
     {
       if (auth->type == AUTH_BASIC || auth->type == AUTH_BASICDIGEST)
        auth_supported = "basic";
       else if (auth->type == AUTH_DIGEST)
        auth_supported = "digest";
+
+      if (auth->type != AUTH_NONE)
+        p->type |= CUPS_PRINTER_AUTHENTICATED;
+      else
+        p->type &= ~CUPS_PRINTER_AUTHENTICATED;
     }
+    else
+      p->type &= ~CUPS_PRINTER_AUTHENTICATED;
   }
 
  /*
index 2ec1570e728aabca101ffa7c30b63cc4dd1e7de9..8334f4f64c244ff03590f034f678dcd0da542c5d 100644 (file)
@@ -60,8 +60,8 @@ function reset_config()
 "    Order deny,allow\\n" +
 "  </Limit>\\n" +
 "\\n" +
-"  # Only the owner or an administrator can cancel a job...\\n" +
-"  <Limit Cancel-Job>\\n" +
+"  # Only the owner or an administrator can cancel or authenticate a job...\\n" +
+"  <Limit Cancel-Job CUPS-Authenticate-Job>\\n" +
 "    Require user @OWNER @SYSTEM\\n" +
 "    Order deny,allow\\n" +
 "  </Limit>\\n" +