{"thread":{"id":"59398","subject":"[PATCH] credential/libsecret: support password_expiry_utc","startedAt":"2023-03-14T21:32:18Z","lastAt":"2023-06-16T19:55:15Z","messageCount":11,"participants":["M Hickford via GitGitGadget","Junio C Hamano","M Hickford"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"473509","messageId":"pull.1469.git.git.1678829531648.gitgitgadget@gmail.com","threadId":"59398","inReplyTo":null,"subject":"[PATCH] credential/libsecret: support password_expiry_utc","fromName":"M Hickford via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2023-03-14T21:32:11Z","receivedAt":"2023-03-14T21:32:18Z","isPatch":true,"sender":{"key":"mirth.hickford@gmail.com","avatar":"https://avatars.githubusercontent.com/u/105314?v=4"},"body":"From: M Hickford <mirth.hickford@gmail.com>\n\nSigned-off-by: M Hickford <mirth.hickford@gmail.com>\n---\n    credential/libsecret: store password_expiry_utc\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1469%2Fhickford%2Flibsecret-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1469/hickford/libsecret-v1\nPull-Request: https://github.com/git/git/pull/1469\n\n .../libsecret/git-credential-libsecret.c      | 42 ++++++++++++++++---\n 1 file changed, 37 insertions(+), 5 deletions(-)\n\ndiff --git a/contrib/credential/libsecret/git-credential-libsecret.c b/contrib/credential/libsecret/git-credential-libsecret.c\nindex 2c5d76d789f..3f2b530db79 100644\n--- a/contrib/credential/libsecret/git-credential-libsecret.c\n+++ b/contrib/credential/libsecret/git-credential-libsecret.c\n@@ -39,6 +39,7 @@ struct credential {\n \tchar *path;\n \tchar *username;\n \tchar *password;\n+\tchar *password_expiry_utc;\n };\n \n #define CREDENTIAL_INIT { 0 }\n@@ -54,6 +55,20 @@ struct credential_operation {\n \n /* ----------------- Secret Service functions ----------------- */\n \n+static const SecretSchema schema = {\n+\t\"org.git.Password\",\n+\tSECRET_SCHEMA_NONE,\n+\t{\n+\t\t{  \"user\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n+\t\t{  \"object\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n+\t\t{  \"protocol\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n+\t\t{  \"port\", SECRET_SCHEMA_ATTRIBUTE_INTEGER },\n+\t\t{  \"server\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n+\t\t{  \"password_expiry_utc\", SECRET_SCHEMA_ATTRIBUTE_INTEGER },\n+\t\t{  NULL, 0 },\n+\t}\n+};\n+\n static char *make_label(struct credential *c)\n {\n \tif (c->port)\n@@ -78,6 +93,9 @@ static GHashTable *make_attr_list(struct credential *c)\n \t\tg_hash_table_insert(al, \"port\", g_strdup_printf(\"%hu\", c->port));\n \tif (c->path)\n \t\tg_hash_table_insert(al, \"object\", g_strdup(c->path));\n+\tif (c->password_expiry_utc)\n+\t\tg_hash_table_insert(al, \"password_expiry_utc\",\n+\t\t\tg_strdup(c->password_expiry_utc));\n \n \treturn al;\n }\n@@ -101,9 +119,11 @@ static int keyring_get(struct credential *c)\n \n \tattributes = make_attr_list(c);\n \titems = secret_service_search_sync(service,\n-\t\t\t\t\t   SECRET_SCHEMA_COMPAT_NETWORK,\n+\t\t\t\t\t   &schema,\n \t\t\t\t\t   attributes,\n-\t\t\t\t\t   SECRET_SEARCH_LOAD_SECRETS | SECRET_SEARCH_UNLOCK,\n+\t\t\t\t\t   SECRET_SEARCH_LOAD_SECRETS | SECRET_SEARCH_UNLOCK |\n+\t\t\t\t\t   // for backwards compatibility\n+\t\t\t\t\t   SECRET_SCHEMA_DONT_MATCH_NAME,\n \t\t\t\t\t   NULL,\n \t\t\t\t\t   &error);\n \tg_hash_table_unref(attributes);\n@@ -128,6 +148,12 @@ static int keyring_get(struct credential *c)\n \t\t\tc->username = g_strdup(s);\n \t\t}\n \n+\t\ts = g_hash_table_lookup(attributes, \"password_expiry_utc\");\n+\t\tif (s) {\n+\t\t\tg_free(c->password_expiry_utc);\n+\t\t\tc->password_expiry_utc = g_strdup(s);\n+\t\t}\n+\n \t\ts = secret_value_get_text(secret);\n \t\tif (s) {\n \t\t\tg_free(c->password);\n@@ -162,7 +188,7 @@ static int keyring_store(struct credential *c)\n \n \tlabel = make_label(c);\n \tattributes = make_attr_list(c);\n-\tsecret_password_storev_sync(SECRET_SCHEMA_COMPAT_NETWORK,\n+\tsecret_password_storev_sync(&schema,\n \t\t\t\t    attributes,\n \t\t\t\t    NULL,\n \t\t\t\t    label,\n@@ -198,7 +224,7 @@ static int keyring_erase(struct credential *c)\n \t\treturn EXIT_FAILURE;\n \n \tattributes = make_attr_list(c);\n-\tsecret_password_clearv_sync(SECRET_SCHEMA_COMPAT_NETWORK,\n+\tsecret_password_clearv_sync(&schema,\n \t\t\t\t    attributes,\n \t\t\t\t    NULL,\n \t\t\t\t    &error);\n@@ -238,6 +264,7 @@ static void credential_clear(struct credential *c)\n \tg_free(c->path);\n \tg_free(c->username);\n \tg_free(c->password);\n+\tg_free(c->password_expiry_utc);\n \n \tcredential_init(c);\n }\n@@ -285,6 +312,9 @@ static int credential_read(struct credential *c)\n \t\t} else if (!strcmp(key, \"username\")) {\n \t\t\tg_free(c->username);\n \t\t\tc->username = g_strdup(value);\n+\t\t} else if (!strcmp(key, \"password_expiry_utc\")) {\n+\t\t\tg_free(c->password_expiry_utc);\n+\t\t\tc->password_expiry_utc = g_strdup(value);\n \t\t} else if (!strcmp(key, \"password\")) {\n \t\t\tg_free(c->password);\n \t\t\tc->password = g_strdup(value);\n@@ -312,9 +342,11 @@ static void credential_write_item(FILE *fp, const char *key, const char *value)\n \n static void credential_write(const struct credential *c)\n {\n-\t/* only write username/password, if set */\n+\t/* only write username/password/expiry, if set */\n \tcredential_write_item(stdout, \"username\", c->username);\n \tcredential_write_item(stdout, \"password\", c->password);\n+\tcredential_write_item(stdout, \"password_expiry_utc\",\n+\t\tc->password_expiry_utc);\n }\n \n static void usage(const char *name)\n\nbase-commit: 73876f4861cd3d187a4682290ab75c9dccadbc56\n-- \ngitgitgadget\n"},{"id":"474152","messageId":"pull.1469.v2.git.git.1679729764851.gitgitgadget@gmail.com","threadId":"59398","inReplyTo":"pull.1469.git.git.1678829531648.gitgitgadget@gmail.com","subject":"[PATCH v2] credential/libsecret: support password_expiry_utc","fromName":"M Hickford via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2023-03-25T07:36:04Z","receivedAt":"2023-03-25T07:36:11Z","isPatch":true,"sender":{"key":"mirth.hickford@gmail.com","avatar":"https://avatars.githubusercontent.com/u/105314?v=4"},"body":"From: M Hickford <mirth.hickford@gmail.com>\n\nSigned-off-by: M Hickford <mirth.hickford@gmail.com>\n---\n    credential/libsecret: store password_expiry_utc\n    \n    Patch v2 adds tests.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1469%2Fhickford%2Flibsecret-v2\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1469/hickford/libsecret-v2\nPull-Request: https://github.com/git/git/pull/1469\n\nRange-diff vs v1:\n\n 1:  46ba4863f33 ! 1:  1e27677b6f5 credential/libsecret: support password_expiry_utc\n     @@ contrib/credential/libsecret/git-credential-libsecret.c: static void credential_\n       }\n       \n       static void usage(const char *name)\n     +\n     + ## t/lib-credential.sh ##\n     +@@ t/lib-credential.sh: helper_test_clean() {\n     + \treject $1 https example.com store-user\n     + \treject $1 https example.com user1\n     + \treject $1 https example.com user2\n     ++\treject $1 https example.com user3\n     + \treject $1 http path.tld user\n     + \treject $1 https timeout.tld user\n     + \treject $1 https sso.tld\n     +@@ t/lib-credential.sh: helper_test_timeout() {\n     + \t'\n     + }\n     + \n     ++helper_test_password_expiry_utc() {\n     ++\tHELPER=$1\n     ++\n     ++\ttest_expect_success \"helper ($HELPER) stores password_expiry_utc\" '\n     ++\t\tcheck approve $HELPER <<-\\EOF\n     ++\t\tprotocol=https\n     ++\t\thost=example.com\n     ++\t\tusername=user3\n     ++\t\tpassword=pass\n     ++\t\tpassword_expiry_utc=9999999999\n     ++\t\tEOF\n     ++\t'\n     ++\n     ++\ttest_expect_success \"helper ($HELPER) gets password_expiry_utc\" '\n     ++\t\tcheck fill $HELPER <<-\\EOF\n     ++\t\tprotocol=https\n     ++\t\thost=example.com\n     ++\t\tusername=user3\n     ++\t\t--\n     ++\t\tprotocol=https\n     ++\t\thost=example.com\n     ++\t\tusername=user3\n     ++\t\tpassword=pass\n     ++\t\tpassword_expiry_utc=9999999999\n     ++\t\t--\n     ++\t\tEOF\n     ++\t'\n     ++}\n     ++\n     + write_script askpass <<\\EOF\n     + echo >&2 askpass: $*\n     + what=$(echo $1 | cut -d\" \" -f1 | tr A-Z a-z | tr -cd a-z)\n     +\n     + ## t/t0301-credential-cache.sh ##\n     +@@ t/t0301-credential-cache.sh: test_atexit 'git credential-cache exit'\n     + # test that the daemon works with no special setup\n     + helper_test cache\n     + \n     ++helper_test_password_expiry_utc cache\n     ++\n     + test_expect_success 'socket defaults to ~/.cache/git/credential/socket' '\n     + \ttest_when_finished \"\n     + \t\tgit credential-cache exit &&\n     +\n     + ## t/t0303-credential-external.sh ##\n     +@@ t/t0303-credential-external.sh: else\n     + \thelper_test_timeout \"$GIT_TEST_CREDENTIAL_HELPER_TIMEOUT\"\n     + fi\n     + \n     ++helper_test_password_expiry_utc \"$GIT_TEST_CREDENTIAL_HELPER\"\n     ++\n     + # clean afterwards so that we are good citizens\n     + # and don't leave cruft in the helper's storage, which\n     + # might be long-term system storage\n\n\n .../libsecret/git-credential-libsecret.c      | 42 ++++++++++++++++---\n t/lib-credential.sh                           | 30 +++++++++++++\n t/t0301-credential-cache.sh                   |  2 +\n t/t0303-credential-external.sh                |  2 +\n 4 files changed, 71 insertions(+), 5 deletions(-)\n\ndiff --git a/contrib/credential/libsecret/git-credential-libsecret.c b/contrib/credential/libsecret/git-credential-libsecret.c\nindex 2c5d76d789f..3f2b530db79 100644\n--- a/contrib/credential/libsecret/git-credential-libsecret.c\n+++ b/contrib/credential/libsecret/git-credential-libsecret.c\n@@ -39,6 +39,7 @@ struct credential {\n \tchar *path;\n \tchar *username;\n \tchar *password;\n+\tchar *password_expiry_utc;\n };\n \n #define CREDENTIAL_INIT { 0 }\n@@ -54,6 +55,20 @@ struct credential_operation {\n \n /* ----------------- Secret Service functions ----------------- */\n \n+static const SecretSchema schema = {\n+\t\"org.git.Password\",\n+\tSECRET_SCHEMA_NONE,\n+\t{\n+\t\t{  \"user\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n+\t\t{  \"object\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n+\t\t{  \"protocol\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n+\t\t{  \"port\", SECRET_SCHEMA_ATTRIBUTE_INTEGER },\n+\t\t{  \"server\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n+\t\t{  \"password_expiry_utc\", SECRET_SCHEMA_ATTRIBUTE_INTEGER },\n+\t\t{  NULL, 0 },\n+\t}\n+};\n+\n static char *make_label(struct credential *c)\n {\n \tif (c->port)\n@@ -78,6 +93,9 @@ static GHashTable *make_attr_list(struct credential *c)\n \t\tg_hash_table_insert(al, \"port\", g_strdup_printf(\"%hu\", c->port));\n \tif (c->path)\n \t\tg_hash_table_insert(al, \"object\", g_strdup(c->path));\n+\tif (c->password_expiry_utc)\n+\t\tg_hash_table_insert(al, \"password_expiry_utc\",\n+\t\t\tg_strdup(c->password_expiry_utc));\n \n \treturn al;\n }\n@@ -101,9 +119,11 @@ static int keyring_get(struct credential *c)\n \n \tattributes = make_attr_list(c);\n \titems = secret_service_search_sync(service,\n-\t\t\t\t\t   SECRET_SCHEMA_COMPAT_NETWORK,\n+\t\t\t\t\t   &schema,\n \t\t\t\t\t   attributes,\n-\t\t\t\t\t   SECRET_SEARCH_LOAD_SECRETS | SECRET_SEARCH_UNLOCK,\n+\t\t\t\t\t   SECRET_SEARCH_LOAD_SECRETS | SECRET_SEARCH_UNLOCK |\n+\t\t\t\t\t   // for backwards compatibility\n+\t\t\t\t\t   SECRET_SCHEMA_DONT_MATCH_NAME,\n \t\t\t\t\t   NULL,\n \t\t\t\t\t   &error);\n \tg_hash_table_unref(attributes);\n@@ -128,6 +148,12 @@ static int keyring_get(struct credential *c)\n \t\t\tc->username = g_strdup(s);\n \t\t}\n \n+\t\ts = g_hash_table_lookup(attributes, \"password_expiry_utc\");\n+\t\tif (s) {\n+\t\t\tg_free(c->password_expiry_utc);\n+\t\t\tc->password_expiry_utc = g_strdup(s);\n+\t\t}\n+\n \t\ts = secret_value_get_text(secret);\n \t\tif (s) {\n \t\t\tg_free(c->password);\n@@ -162,7 +188,7 @@ static int keyring_store(struct credential *c)\n \n \tlabel = make_label(c);\n \tattributes = make_attr_list(c);\n-\tsecret_password_storev_sync(SECRET_SCHEMA_COMPAT_NETWORK,\n+\tsecret_password_storev_sync(&schema,\n \t\t\t\t    attributes,\n \t\t\t\t    NULL,\n \t\t\t\t    label,\n@@ -198,7 +224,7 @@ static int keyring_erase(struct credential *c)\n \t\treturn EXIT_FAILURE;\n \n \tattributes = make_attr_list(c);\n-\tsecret_password_clearv_sync(SECRET_SCHEMA_COMPAT_NETWORK,\n+\tsecret_password_clearv_sync(&schema,\n \t\t\t\t    attributes,\n \t\t\t\t    NULL,\n \t\t\t\t    &error);\n@@ -238,6 +264,7 @@ static void credential_clear(struct credential *c)\n \tg_free(c->path);\n \tg_free(c->username);\n \tg_free(c->password);\n+\tg_free(c->password_expiry_utc);\n \n \tcredential_init(c);\n }\n@@ -285,6 +312,9 @@ static int credential_read(struct credential *c)\n \t\t} else if (!strcmp(key, \"username\")) {\n \t\t\tg_free(c->username);\n \t\t\tc->username = g_strdup(value);\n+\t\t} else if (!strcmp(key, \"password_expiry_utc\")) {\n+\t\t\tg_free(c->password_expiry_utc);\n+\t\t\tc->password_expiry_utc = g_strdup(value);\n \t\t} else if (!strcmp(key, \"password\")) {\n \t\t\tg_free(c->password);\n \t\t\tc->password = g_strdup(value);\n@@ -312,9 +342,11 @@ static void credential_write_item(FILE *fp, const char *key, const char *value)\n \n static void credential_write(const struct credential *c)\n {\n-\t/* only write username/password, if set */\n+\t/* only write username/password/expiry, if set */\n \tcredential_write_item(stdout, \"username\", c->username);\n \tcredential_write_item(stdout, \"password\", c->password);\n+\tcredential_write_item(stdout, \"password_expiry_utc\",\n+\t\tc->password_expiry_utc);\n }\n \n static void usage(const char *name)\ndiff --git a/t/lib-credential.sh b/t/lib-credential.sh\nindex 5ea8bc9f1dc..9ebf7eeae48 100644\n--- a/t/lib-credential.sh\n+++ b/t/lib-credential.sh\n@@ -43,6 +43,7 @@ helper_test_clean() {\n \treject $1 https example.com store-user\n \treject $1 https example.com user1\n \treject $1 https example.com user2\n+\treject $1 https example.com user3\n \treject $1 http path.tld user\n \treject $1 https timeout.tld user\n \treject $1 https sso.tld\n@@ -298,6 +299,35 @@ helper_test_timeout() {\n \t'\n }\n \n+helper_test_password_expiry_utc() {\n+\tHELPER=$1\n+\n+\ttest_expect_success \"helper ($HELPER) stores password_expiry_utc\" '\n+\t\tcheck approve $HELPER <<-\\EOF\n+\t\tprotocol=https\n+\t\thost=example.com\n+\t\tusername=user3\n+\t\tpassword=pass\n+\t\tpassword_expiry_utc=9999999999\n+\t\tEOF\n+\t'\n+\n+\ttest_expect_success \"helper ($HELPER) gets password_expiry_utc\" '\n+\t\tcheck fill $HELPER <<-\\EOF\n+\t\tprotocol=https\n+\t\thost=example.com\n+\t\tusername=user3\n+\t\t--\n+\t\tprotocol=https\n+\t\thost=example.com\n+\t\tusername=user3\n+\t\tpassword=pass\n+\t\tpassword_expiry_utc=9999999999\n+\t\t--\n+\t\tEOF\n+\t'\n+}\n+\n write_script askpass <<\\EOF\n echo >&2 askpass: $*\n what=$(echo $1 | cut -d\" \" -f1 | tr A-Z a-z | tr -cd a-z)\ndiff --git a/t/t0301-credential-cache.sh b/t/t0301-credential-cache.sh\nindex 698b7159f03..f5ba727e53b 100755\n--- a/t/t0301-credential-cache.sh\n+++ b/t/t0301-credential-cache.sh\n@@ -30,6 +30,8 @@ test_atexit 'git credential-cache exit'\n # test that the daemon works with no special setup\n helper_test cache\n \n+helper_test_password_expiry_utc cache\n+\n test_expect_success 'socket defaults to ~/.cache/git/credential/socket' '\n \ttest_when_finished \"\n \t\tgit credential-cache exit &&\ndiff --git a/t/t0303-credential-external.sh b/t/t0303-credential-external.sh\nindex f028fd14182..f1478680bff 100755\n--- a/t/t0303-credential-external.sh\n+++ b/t/t0303-credential-external.sh\n@@ -52,6 +52,8 @@ else\n \thelper_test_timeout \"$GIT_TEST_CREDENTIAL_HELPER_TIMEOUT\"\n fi\n \n+helper_test_password_expiry_utc \"$GIT_TEST_CREDENTIAL_HELPER\"\n+\n # clean afterwards so that we are good citizens\n # and don't leave cruft in the helper's storage, which\n # might be long-term system storage\n\nbase-commit: 27d43aaaf50ef0ae014b88bba294f93658016a2e\n-- \ngitgitgadget\n"},{"id":"476603","messageId":"xmqqjzxoj8mm.fsf@gitster.g","threadId":"59398","inReplyTo":"pull.1469.v2.git.git.1679729764851.gitgitgadget@gmail.com","subject":"Re: [PATCH v2] credential/libsecret: support password_expiry_utc","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-05-04T17:42:41Z","receivedAt":"2023-05-04T17:42:47Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"M Hickford via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: M Hickford <mirth.hickford@gmail.com>\n>\n> Signed-off-by: M Hickford <mirth.hickford@gmail.com>\n> ---\n> diff --git a/contrib/credential/libsecret/git-credential-libsecret.c b/contrib/credential/libsecret/git-credential-libsecret.c\n> index 2c5d76d789f..3f2b530db79 100644\n> --- a/contrib/credential/libsecret/git-credential-libsecret.c\n> +++ b/contrib/credential/libsecret/git-credential-libsecret.c\n> @@ -39,6 +39,7 @@ struct credential {\n>  \tchar *path;\n>  \tchar *username;\n>  \tchar *password;\n> +\tchar *password_expiry_utc;\n>  };\n>  \n>  #define CREDENTIAL_INIT { 0 }\n> @@ -54,6 +55,20 @@ struct credential_operation {\n>  \n>  /* ----------------- Secret Service functions ----------------- */\n>  \n> +static const SecretSchema schema = {\n> +\t\"org.git.Password\",\n> +\tSECRET_SCHEMA_NONE,\n> +\t{\n> +\t\t{  \"user\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n> +\t\t{  \"object\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n> +\t\t{  \"protocol\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n> +\t\t{  \"port\", SECRET_SCHEMA_ATTRIBUTE_INTEGER },\n> +\t\t{  \"server\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n> +\t\t{  \"password_expiry_utc\", SECRET_SCHEMA_ATTRIBUTE_INTEGER },\n> +\t\t{  NULL, 0 },\n> +\t}\n> +};\n\nWe used to use the bog-standard \"COMPAT_NETWORK\" but now we are\nadding an extra element, and that makes it necessary to define our\nown?  OK.\n\n>  static char *make_label(struct credential *c)\n>  {\n>  \tif (c->port)\n> @@ -78,6 +93,9 @@ static GHashTable *make_attr_list(struct credential *c)\n>  \t\tg_hash_table_insert(al, \"port\", g_strdup_printf(\"%hu\", c->port));\n>  \tif (c->path)\n>  \t\tg_hash_table_insert(al, \"object\", g_strdup(c->path));\n> +\tif (c->password_expiry_utc)\n> +\t\tg_hash_table_insert(al, \"password_expiry_utc\",\n> +\t\t\tg_strdup(c->password_expiry_utc));\n>  \n>  \treturn al;\n>  }\n> @@ -101,9 +119,11 @@ static int keyring_get(struct credential *c)\n>  \n>  \tattributes = make_attr_list(c);\n>  \titems = secret_service_search_sync(service,\n> -\t\t\t\t\t   SECRET_SCHEMA_COMPAT_NETWORK,\n> +\t\t\t\t\t   &schema,\n>  \t\t\t\t\t   attributes,\n> -\t\t\t\t\t   SECRET_SEARCH_LOAD_SECRETS | SECRET_SEARCH_UNLOCK,\n> +\t\t\t\t\t   SECRET_SEARCH_LOAD_SECRETS | SECRET_SEARCH_UNLOCK |\n> +\t\t\t\t\t   // for backwards compatibility\n\nNo // comments please.\n\n> +\t\t\t\t\t   SECRET_SCHEMA_DONT_MATCH_NAME,\n\nSECRET_SCHEMA_DONT_MATCH_NAME does not seem to be listed as one of\nthe flags to be used with secret_service_search_sync(),\n\n    https://www.manpagez.com/html/libsecret-1/libsecret-1-0.18.6/SecretService.php#secret-service-search-sync\n\nand the only reference to it I found was as a flag to be placed in\nthe schema.\n\n    https://www.manpagez.com/html/libsecret-1/libsecret-1-0.18.6/migrating-schemas.php\n    https://www.manpagez.com/html/libsecret-1/libsecret-1-0.18.6/libsecret-SecretSchema.php\n\nBut I'll take your word for it.\n\nI found nothing unexpected or surprising in the rest of the patch to\nthis file.  They all looked just a fallout of having to store and\nretrieve one extra item from the database together with many other\nthings we already store and retrieve.  Cleanly written.\n\n> diff --git a/t/lib-credential.sh b/t/lib-credential.sh\n> index 5ea8bc9f1dc..9ebf7eeae48 100644\n> --- a/t/lib-credential.sh\n> +++ b/t/lib-credential.sh\n> @@ -43,6 +43,7 @@ helper_test_clean() {\n>  \treject $1 https example.com store-user\n>  \treject $1 https example.com user1\n>  \treject $1 https example.com user2\n> +\treject $1 https example.com user3\n>  \treject $1 http path.tld user\n>  \treject $1 https timeout.tld user\n>  \treject $1 https sso.tld\n> @@ -298,6 +299,35 @@ helper_test_timeout() {\n>  \t'\n>  }\n>  \n> +helper_test_password_expiry_utc() {\n> +\tHELPER=$1\n> +\n> +\ttest_expect_success \"helper ($HELPER) stores password_expiry_utc\" '\n> +\t\tcheck approve $HELPER <<-\\EOF\n> +\t\tprotocol=https\n> +\t\thost=example.com\n> +\t\tusername=user3\n> +\t\tpassword=pass\n> +\t\tpassword_expiry_utc=9999999999\n> +\t\tEOF\n> +\t'\n>\n> +\ttest_expect_success \"helper ($HELPER) gets password_expiry_utc\" '\n> +\t\tcheck fill $HELPER <<-\\EOF\n> +\t\tprotocol=https\n> +\t\thost=example.com\n> +\t\tusername=user3\n> +\t\t--\n> +\t\tprotocol=https\n> +\t\thost=example.com\n> +\t\tusername=user3\n> +\t\tpassword=pass\n> +\t\tpassword_expiry_utc=9999999999\n> +\t\t--\n> +\t\tEOF\n> +\t'\n> +}\n> +\n\nIs any random number usable for this test, or is there some\nconstraints (like, \"it cannot be too small to be a timestamp in the\npast, because the entry will be expired immediately\")?  If there is\nsome constraint like that, is it a good idea to also test that\n(like, \"make sure an entry with expiry already happened is\nrejected\")?\n\nThanks.\n\n"},{"id":"476617","messageId":"CAGJzqsmL6AK8BsxJJzRRddcdep-F2szUAvE-2ep5KBsmUVnghw@mail.gmail.com","threadId":"59398","inReplyTo":"xmqqjzxoj8mm.fsf@gitster.g","subject":"Re: [PATCH v2] credential/libsecret: support password_expiry_utc","fromName":"M Hickford","fromEmail":"mirth.hickford@gmail.com","sentAt":"2023-05-05T07:00:31Z","receivedAt":"2023-05-05T07:01:12Z","isPatch":true,"sender":{"key":"mirth.hickford@gmail.com","avatar":"https://avatars.githubusercontent.com/u/105314?v=4"},"body":"On Thu, 4 May 2023 at 18:42, Junio C Hamano <gitster@pobox.com> wrote:\n>\n> \"M Hickford via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>\n> We used to use the bog-standard \"COMPAT_NETWORK\" but now we are\n> adding an extra element, and that makes it necessary to define our\n> own?  OK.\n\nExactly.\n\n>\n> >  static char *make_label(struct credential *c)\n> >  {\n> >       if (c->port)\n> > @@ -78,6 +93,9 @@ static GHashTable *make_attr_list(struct credential *c)\n> >               g_hash_table_insert(al, \"port\", g_strdup_printf(\"%hu\", c->port));\n> >       if (c->path)\n> >               g_hash_table_insert(al, \"object\", g_strdup(c->path));\n> > +     if (c->password_expiry_utc)\n> > +             g_hash_table_insert(al, \"password_expiry_utc\",\n> > +                     g_strdup(c->password_expiry_utc));\n> >\n> >       return al;\n> >  }\n> > @@ -101,9 +119,11 @@ static int keyring_get(struct credential *c)\n> >\n> >       attributes = make_attr_list(c);\n> >       items = secret_service_search_sync(service,\n> > -                                        SECRET_SCHEMA_COMPAT_NETWORK,\n> > +                                        &schema,\n> >                                          attributes,\n> > -                                        SECRET_SEARCH_LOAD_SECRETS | SECRET_SEARCH_UNLOCK,\n> > +                                        SECRET_SEARCH_LOAD_SECRETS | SECRET_SEARCH_UNLOCK |\n> > +                                        // for backwards compatibility\n>\n> No // comments please.\n\nI'll amend in v3.\n\n>\n> > +                                        SECRET_SCHEMA_DONT_MATCH_NAME,\n>\n> SECRET_SCHEMA_DONT_MATCH_NAME does not seem to be listed as one of\n> the flags to be used with secret_service_search_sync(),\n>\n>     https://www.manpagez.com/html/libsecret-1/libsecret-1-0.18.6/SecretService.php#secret-service-search-sync\n>\n> and the only reference to it I found was as a flag to be placed in\n> the schema.\n>\n>     https://www.manpagez.com/html/libsecret-1/libsecret-1-0.18.6/migrating-schemas.php\n>     https://www.manpagez.com/html/libsecret-1/libsecret-1-0.18.6/libsecret-SecretSchema.php\n>\n> But I'll take your word for it.\n\nGood spot. I must have misread the docs. I shall fix in v3 and test\nbackwards compatibility carefully.\n\n[1] https://gnome.pages.gitlab.gnome.org/libsecret/migrating-libgnome-keyring.html#working-with-schemas\n\n>\n> I found nothing unexpected or surprising in the rest of the patch to\n> this file.  They all looked just a fallout of having to store and\n> retrieve one extra item from the database together with many other\n> things we already store and retrieve.  Cleanly written.\n\nThanks Junio for the review.\n\n>\n> > diff --git a/t/lib-credential.sh b/t/lib-credential.sh\n> > index 5ea8bc9f1dc..9ebf7eeae48 100644\n> > --- a/t/lib-credential.sh\n> > +++ b/t/lib-credential.sh\n> > @@ -43,6 +43,7 @@ helper_test_clean() {\n> >       reject $1 https example.com store-user\n> >       reject $1 https example.com user1\n> >       reject $1 https example.com user2\n> > +     reject $1 https example.com user3\n> >       reject $1 http path.tld user\n> >       reject $1 https timeout.tld user\n> >       reject $1 https sso.tld\n> > @@ -298,6 +299,35 @@ helper_test_timeout() {\n> >       '\n> >  }\n> >\n> > +helper_test_password_expiry_utc() {\n> > +     HELPER=$1\n> > +\n> > +     test_expect_success \"helper ($HELPER) stores password_expiry_utc\" '\n> > +             check approve $HELPER <<-\\EOF\n> > +             protocol=https\n> > +             host=example.com\n> > +             username=user3\n> > +             password=pass\n> > +             password_expiry_utc=9999999999\n> > +             EOF\n> > +     '\n> >\n> > +     test_expect_success \"helper ($HELPER) gets password_expiry_utc\" '\n> > +             check fill $HELPER <<-\\EOF\n> > +             protocol=https\n> > +             host=example.com\n> > +             username=user3\n> > +             --\n> > +             protocol=https\n> > +             host=example.com\n> > +             username=user3\n> > +             password=pass\n> > +             password_expiry_utc=9999999999\n> > +             --\n> > +             EOF\n> > +     '\n> > +}\n> > +\n>\n> Is any random number usable for this test, or is there some\n> constraints (like, \"it cannot be too small to be a timestamp in the\n> past, because the entry will be expired immediately\")?  If there is\n> some constraint like that, is it a good idea to also test that\n> (like, \"make sure an entry with expiry already happened is\n> rejected\")?\n\nThe date has to be in the future otherwise the credential will be\ndiscarded by `credential approve` before it reaches the helper. That\nlogic is already tested in t/t0300-credentials.sh. There isn't any\nexpiry logic in the helper itself to test.\n\n>\n> Thanks.\n>\n"},{"id":"476618","messageId":"pull.1469.v3.git.git.1683270298313.gitgitgadget@gmail.com","threadId":"59398","inReplyTo":"pull.1469.v2.git.git.1679729764851.gitgitgadget@gmail.com","subject":"[PATCH v3] credential/libsecret: support password_expiry_utc","fromName":"M Hickford via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2023-05-05T07:04:57Z","receivedAt":"2023-05-05T07:05:05Z","isPatch":true,"sender":{"key":"mirth.hickford@gmail.com","avatar":"https://avatars.githubusercontent.com/u/105314?v=4"},"body":"From: M Hickford <mirth.hickford@gmail.com>\n\nd208bfd (credential: new attribute password_expiry_utc, 2023-02-18)\nintroduced this attribute.\n\nSigned-off-by: M Hickford <mirth.hickford@gmail.com>\n---\n    credential/libsecret: store password_expiry_utc\n    \n    Patch v3 fixes backwards compatibility.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1469%2Fhickford%2Flibsecret-v3\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1469/hickford/libsecret-v3\nPull-Request: https://github.com/git/git/pull/1469\n\nRange-diff vs v2:\n\n 1:  1e27677b6f5 ! 1:  b46594c8897 credential/libsecret: support password_expiry_utc\n     @@ Metadata\n       ## Commit message ##\n          credential/libsecret: support password_expiry_utc\n      \n     +    d208bfd (credential: new attribute password_expiry_utc, 2023-02-18)\n     +    introduced this attribute.\n     +\n          Signed-off-by: M Hickford <mirth.hickford@gmail.com>\n      \n       ## contrib/credential/libsecret/git-credential-libsecret.c ##\n     @@ contrib/credential/libsecret/git-credential-libsecret.c: struct credential_opera\n       \n      +static const SecretSchema schema = {\n      +\t\"org.git.Password\",\n     -+\tSECRET_SCHEMA_NONE,\n     ++\t/* Ignore schema name for backwards compatibility with previous versions */\n     ++\tSECRET_SCHEMA_DONT_MATCH_NAME,\n      +\t{\n      +\t\t{  \"user\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n      +\t\t{  \"object\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n     @@ contrib/credential/libsecret/git-credential-libsecret.c: static int keyring_get(\n      -\t\t\t\t\t   SECRET_SCHEMA_COMPAT_NETWORK,\n      +\t\t\t\t\t   &schema,\n       \t\t\t\t\t   attributes,\n     --\t\t\t\t\t   SECRET_SEARCH_LOAD_SECRETS | SECRET_SEARCH_UNLOCK,\n     -+\t\t\t\t\t   SECRET_SEARCH_LOAD_SECRETS | SECRET_SEARCH_UNLOCK |\n     -+\t\t\t\t\t   // for backwards compatibility\n     -+\t\t\t\t\t   SECRET_SCHEMA_DONT_MATCH_NAME,\n     + \t\t\t\t\t   SECRET_SEARCH_LOAD_SECRETS | SECRET_SEARCH_UNLOCK,\n       \t\t\t\t\t   NULL,\n     - \t\t\t\t\t   &error);\n     - \tg_hash_table_unref(attributes);\n      @@ contrib/credential/libsecret/git-credential-libsecret.c: static int keyring_get(struct credential *c)\n       \t\t\tc->username = g_strdup(s);\n       \t\t}\n\n\n .../libsecret/git-credential-libsecret.c      | 39 +++++++++++++++++--\n t/lib-credential.sh                           | 30 ++++++++++++++\n t/t0301-credential-cache.sh                   |  2 +\n t/t0303-credential-external.sh                |  2 +\n 4 files changed, 69 insertions(+), 4 deletions(-)\n\ndiff --git a/contrib/credential/libsecret/git-credential-libsecret.c b/contrib/credential/libsecret/git-credential-libsecret.c\nindex 2c5d76d789f..182f0805c2b 100644\n--- a/contrib/credential/libsecret/git-credential-libsecret.c\n+++ b/contrib/credential/libsecret/git-credential-libsecret.c\n@@ -39,6 +39,7 @@ struct credential {\n \tchar *path;\n \tchar *username;\n \tchar *password;\n+\tchar *password_expiry_utc;\n };\n \n #define CREDENTIAL_INIT { 0 }\n@@ -54,6 +55,21 @@ struct credential_operation {\n \n /* ----------------- Secret Service functions ----------------- */\n \n+static const SecretSchema schema = {\n+\t\"org.git.Password\",\n+\t/* Ignore schema name for backwards compatibility with previous versions */\n+\tSECRET_SCHEMA_DONT_MATCH_NAME,\n+\t{\n+\t\t{  \"user\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n+\t\t{  \"object\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n+\t\t{  \"protocol\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n+\t\t{  \"port\", SECRET_SCHEMA_ATTRIBUTE_INTEGER },\n+\t\t{  \"server\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n+\t\t{  \"password_expiry_utc\", SECRET_SCHEMA_ATTRIBUTE_INTEGER },\n+\t\t{  NULL, 0 },\n+\t}\n+};\n+\n static char *make_label(struct credential *c)\n {\n \tif (c->port)\n@@ -78,6 +94,9 @@ static GHashTable *make_attr_list(struct credential *c)\n \t\tg_hash_table_insert(al, \"port\", g_strdup_printf(\"%hu\", c->port));\n \tif (c->path)\n \t\tg_hash_table_insert(al, \"object\", g_strdup(c->path));\n+\tif (c->password_expiry_utc)\n+\t\tg_hash_table_insert(al, \"password_expiry_utc\",\n+\t\t\tg_strdup(c->password_expiry_utc));\n \n \treturn al;\n }\n@@ -101,7 +120,7 @@ static int keyring_get(struct credential *c)\n \n \tattributes = make_attr_list(c);\n \titems = secret_service_search_sync(service,\n-\t\t\t\t\t   SECRET_SCHEMA_COMPAT_NETWORK,\n+\t\t\t\t\t   &schema,\n \t\t\t\t\t   attributes,\n \t\t\t\t\t   SECRET_SEARCH_LOAD_SECRETS | SECRET_SEARCH_UNLOCK,\n \t\t\t\t\t   NULL,\n@@ -128,6 +147,12 @@ static int keyring_get(struct credential *c)\n \t\t\tc->username = g_strdup(s);\n \t\t}\n \n+\t\ts = g_hash_table_lookup(attributes, \"password_expiry_utc\");\n+\t\tif (s) {\n+\t\t\tg_free(c->password_expiry_utc);\n+\t\t\tc->password_expiry_utc = g_strdup(s);\n+\t\t}\n+\n \t\ts = secret_value_get_text(secret);\n \t\tif (s) {\n \t\t\tg_free(c->password);\n@@ -162,7 +187,7 @@ static int keyring_store(struct credential *c)\n \n \tlabel = make_label(c);\n \tattributes = make_attr_list(c);\n-\tsecret_password_storev_sync(SECRET_SCHEMA_COMPAT_NETWORK,\n+\tsecret_password_storev_sync(&schema,\n \t\t\t\t    attributes,\n \t\t\t\t    NULL,\n \t\t\t\t    label,\n@@ -198,7 +223,7 @@ static int keyring_erase(struct credential *c)\n \t\treturn EXIT_FAILURE;\n \n \tattributes = make_attr_list(c);\n-\tsecret_password_clearv_sync(SECRET_SCHEMA_COMPAT_NETWORK,\n+\tsecret_password_clearv_sync(&schema,\n \t\t\t\t    attributes,\n \t\t\t\t    NULL,\n \t\t\t\t    &error);\n@@ -238,6 +263,7 @@ static void credential_clear(struct credential *c)\n \tg_free(c->path);\n \tg_free(c->username);\n \tg_free(c->password);\n+\tg_free(c->password_expiry_utc);\n \n \tcredential_init(c);\n }\n@@ -285,6 +311,9 @@ static int credential_read(struct credential *c)\n \t\t} else if (!strcmp(key, \"username\")) {\n \t\t\tg_free(c->username);\n \t\t\tc->username = g_strdup(value);\n+\t\t} else if (!strcmp(key, \"password_expiry_utc\")) {\n+\t\t\tg_free(c->password_expiry_utc);\n+\t\t\tc->password_expiry_utc = g_strdup(value);\n \t\t} else if (!strcmp(key, \"password\")) {\n \t\t\tg_free(c->password);\n \t\t\tc->password = g_strdup(value);\n@@ -312,9 +341,11 @@ static void credential_write_item(FILE *fp, const char *key, const char *value)\n \n static void credential_write(const struct credential *c)\n {\n-\t/* only write username/password, if set */\n+\t/* only write username/password/expiry, if set */\n \tcredential_write_item(stdout, \"username\", c->username);\n \tcredential_write_item(stdout, \"password\", c->password);\n+\tcredential_write_item(stdout, \"password_expiry_utc\",\n+\t\tc->password_expiry_utc);\n }\n \n static void usage(const char *name)\ndiff --git a/t/lib-credential.sh b/t/lib-credential.sh\nindex 5ea8bc9f1dc..9ebf7eeae48 100644\n--- a/t/lib-credential.sh\n+++ b/t/lib-credential.sh\n@@ -43,6 +43,7 @@ helper_test_clean() {\n \treject $1 https example.com store-user\n \treject $1 https example.com user1\n \treject $1 https example.com user2\n+\treject $1 https example.com user3\n \treject $1 http path.tld user\n \treject $1 https timeout.tld user\n \treject $1 https sso.tld\n@@ -298,6 +299,35 @@ helper_test_timeout() {\n \t'\n }\n \n+helper_test_password_expiry_utc() {\n+\tHELPER=$1\n+\n+\ttest_expect_success \"helper ($HELPER) stores password_expiry_utc\" '\n+\t\tcheck approve $HELPER <<-\\EOF\n+\t\tprotocol=https\n+\t\thost=example.com\n+\t\tusername=user3\n+\t\tpassword=pass\n+\t\tpassword_expiry_utc=9999999999\n+\t\tEOF\n+\t'\n+\n+\ttest_expect_success \"helper ($HELPER) gets password_expiry_utc\" '\n+\t\tcheck fill $HELPER <<-\\EOF\n+\t\tprotocol=https\n+\t\thost=example.com\n+\t\tusername=user3\n+\t\t--\n+\t\tprotocol=https\n+\t\thost=example.com\n+\t\tusername=user3\n+\t\tpassword=pass\n+\t\tpassword_expiry_utc=9999999999\n+\t\t--\n+\t\tEOF\n+\t'\n+}\n+\n write_script askpass <<\\EOF\n echo >&2 askpass: $*\n what=$(echo $1 | cut -d\" \" -f1 | tr A-Z a-z | tr -cd a-z)\ndiff --git a/t/t0301-credential-cache.sh b/t/t0301-credential-cache.sh\nindex 698b7159f03..f5ba727e53b 100755\n--- a/t/t0301-credential-cache.sh\n+++ b/t/t0301-credential-cache.sh\n@@ -30,6 +30,8 @@ test_atexit 'git credential-cache exit'\n # test that the daemon works with no special setup\n helper_test cache\n \n+helper_test_password_expiry_utc cache\n+\n test_expect_success 'socket defaults to ~/.cache/git/credential/socket' '\n \ttest_when_finished \"\n \t\tgit credential-cache exit &&\ndiff --git a/t/t0303-credential-external.sh b/t/t0303-credential-external.sh\nindex f028fd14182..f1478680bff 100755\n--- a/t/t0303-credential-external.sh\n+++ b/t/t0303-credential-external.sh\n@@ -52,6 +52,8 @@ else\n \thelper_test_timeout \"$GIT_TEST_CREDENTIAL_HELPER_TIMEOUT\"\n fi\n \n+helper_test_password_expiry_utc \"$GIT_TEST_CREDENTIAL_HELPER\"\n+\n # clean afterwards so that we are good citizens\n # and don't leave cruft in the helper's storage, which\n # might be long-term system storage\n\nbase-commit: 27d43aaaf50ef0ae014b88bba294f93658016a2e\n-- \ngitgitgadget\n"},{"id":"477280","messageId":"CAGJzqskMwOJkriH6serqdwAVYi+fftEL8ohJd-suP6v+OxB_bg@mail.gmail.com","threadId":"59398","inReplyTo":"pull.1469.v3.git.git.1683270298313.gitgitgadget@gmail.com","subject":"Re: [PATCH v3] credential/libsecret: support password_expiry_utc","fromName":"M Hickford","fromEmail":"mirth.hickford@gmail.com","sentAt":"2023-05-15T10:50:01Z","receivedAt":"2023-05-15T10:50:45Z","isPatch":true,"sender":{"key":"mirth.hickford@gmail.com","avatar":"https://avatars.githubusercontent.com/u/105314?v=4"},"body":"On Fri, 5 May 2023 at 08:05, M Hickford via GitGitGadget\n<gitgitgadget@gmail.com> wrote:\n>\n> From: M Hickford <mirth.hickford@gmail.com>\n>\n> d208bfd (credential: new attribute password_expiry_utc, 2023-02-18)\n> introduced this attribute.\n>\n> Signed-off-by: M Hickford <mirth.hickford@gmail.com>\n> ---\n>     credential/libsecret: store password_expiry_utc\n>\n>     Patch v3 fixes backwards compatibility.\n>\n> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1469%2Fhickford%2Flibsecret-v3\n> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1469/hickford/libsecret-v3\n> Pull-Request: https://github.com/git/git/pull/1469\n>\n> Range-diff vs v2:\n>\n>  1:  1e27677b6f5 ! 1:  b46594c8897 credential/libsecret: support password_expiry_utc\n>      @@ Metadata\n>        ## Commit message ##\n>           credential/libsecret: support password_expiry_utc\n>\n>      +    d208bfd (credential: new attribute password_expiry_utc, 2023-02-18)\n>      +    introduced this attribute.\n>      +\n>           Signed-off-by: M Hickford <mirth.hickford@gmail.com>\n>\n>        ## contrib/credential/libsecret/git-credential-libsecret.c ##\n>      @@ contrib/credential/libsecret/git-credential-libsecret.c: struct credential_opera\n>\n>       +static const SecretSchema schema = {\n>       + \"org.git.Password\",\n>      -+ SECRET_SCHEMA_NONE,\n>      ++ /* Ignore schema name for backwards compatibility with previous versions */\n>      ++ SECRET_SCHEMA_DONT_MATCH_NAME,\n>       + {\n>       +         {  \"user\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n>       +         {  \"object\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n>      @@ contrib/credential/libsecret/git-credential-libsecret.c: static int keyring_get(\n>       -                                    SECRET_SCHEMA_COMPAT_NETWORK,\n>       +                                    &schema,\n>                                            attributes,\n>      --                                    SECRET_SEARCH_LOAD_SECRETS | SECRET_SEARCH_UNLOCK,\n>      -+                                    SECRET_SEARCH_LOAD_SECRETS | SECRET_SEARCH_UNLOCK |\n>      -+                                    // for backwards compatibility\n>      -+                                    SECRET_SCHEMA_DONT_MATCH_NAME,\n>      +                                     SECRET_SEARCH_LOAD_SECRETS | SECRET_SEARCH_UNLOCK,\n>                                            NULL,\n>      -                                     &error);\n>      -  g_hash_table_unref(attributes);\n>       @@ contrib/credential/libsecret/git-credential-libsecret.c: static int keyring_get(struct credential *c)\n>                         c->username = g_strdup(s);\n>                 }\n>\n>\n>  .../libsecret/git-credential-libsecret.c      | 39 +++++++++++++++++--\n>  t/lib-credential.sh                           | 30 ++++++++++++++\n>  t/t0301-credential-cache.sh                   |  2 +\n>  t/t0303-credential-external.sh                |  2 +\n>  4 files changed, 69 insertions(+), 4 deletions(-)\n>\n> diff --git a/contrib/credential/libsecret/git-credential-libsecret.c b/contrib/credential/libsecret/git-credential-libsecret.c\n> index 2c5d76d789f..182f0805c2b 100644\n> --- a/contrib/credential/libsecret/git-credential-libsecret.c\n> +++ b/contrib/credential/libsecret/git-credential-libsecret.c\n> @@ -39,6 +39,7 @@ struct credential {\n>         char *path;\n>         char *username;\n>         char *password;\n> +       char *password_expiry_utc;\n>  };\n>\n>  #define CREDENTIAL_INIT { 0 }\n> @@ -54,6 +55,21 @@ struct credential_operation {\n>\n>  /* ----------------- Secret Service functions ----------------- */\n>\n> +static const SecretSchema schema = {\n> +       \"org.git.Password\",\n> +       /* Ignore schema name for backwards compatibility with previous versions */\n> +       SECRET_SCHEMA_DONT_MATCH_NAME,\n> +       {\n> +               {  \"user\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n> +               {  \"object\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n> +               {  \"protocol\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n> +               {  \"port\", SECRET_SCHEMA_ATTRIBUTE_INTEGER },\n> +               {  \"server\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n> +               {  \"password_expiry_utc\", SECRET_SCHEMA_ATTRIBUTE_INTEGER },\n\nI've been testing this patch with credential-generating helper\ngit-credential-helper. It works, but because libsecret overwrites\nitems if and only if the attributes match exactly, you end up with\nmany items in the secret store that differ only by expiry date. This\nis inelegant, and confusing to users. Please hold this patch, don't\nmerge to master. A solution might be to store the expiry date as the\nsecret of a separate item (even though the value is not confidential)\n\n> +               {  NULL, 0 },\n> +       }\n> +};\n> +\n>  static char *make_label(struct credential *c)\n>  {\n>         if (c->port)\n> @@ -78,6 +94,9 @@ static GHashTable *make_attr_list(struct credential *c)\n>                 g_hash_table_insert(al, \"port\", g_strdup_printf(\"%hu\", c->port));\n>         if (c->path)\n>                 g_hash_table_insert(al, \"object\", g_strdup(c->path));\n> +       if (c->password_expiry_utc)\n> +               g_hash_table_insert(al, \"password_expiry_utc\",\n> +                       g_strdup(c->password_expiry_utc));\n>\n>         return al;\n>  }\n> @@ -101,7 +120,7 @@ static int keyring_get(struct credential *c)\n>\n>         attributes = make_attr_list(c);\n>         items = secret_service_search_sync(service,\n> -                                          SECRET_SCHEMA_COMPAT_NETWORK,\n> +                                          &schema,\n>                                            attributes,\n>                                            SECRET_SEARCH_LOAD_SECRETS | SECRET_SEARCH_UNLOCK,\n>                                            NULL,\n> @@ -128,6 +147,12 @@ static int keyring_get(struct credential *c)\n>                         c->username = g_strdup(s);\n>                 }\n>\n> +               s = g_hash_table_lookup(attributes, \"password_expiry_utc\");\n> +               if (s) {\n> +                       g_free(c->password_expiry_utc);\n> +                       c->password_expiry_utc = g_strdup(s);\n> +               }\n> +\n>                 s = secret_value_get_text(secret);\n>                 if (s) {\n>                         g_free(c->password);\n> @@ -162,7 +187,7 @@ static int keyring_store(struct credential *c)\n>\n>         label = make_label(c);\n>         attributes = make_attr_list(c);\n> -       secret_password_storev_sync(SECRET_SCHEMA_COMPAT_NETWORK,\n> +       secret_password_storev_sync(&schema,\n>                                     attributes,\n>                                     NULL,\n>                                     label,\n> @@ -198,7 +223,7 @@ static int keyring_erase(struct credential *c)\n>                 return EXIT_FAILURE;\n>\n>         attributes = make_attr_list(c);\n> -       secret_password_clearv_sync(SECRET_SCHEMA_COMPAT_NETWORK,\n> +       secret_password_clearv_sync(&schema,\n>                                     attributes,\n>                                     NULL,\n>                                     &error);\n> @@ -238,6 +263,7 @@ static void credential_clear(struct credential *c)\n>         g_free(c->path);\n>         g_free(c->username);\n>         g_free(c->password);\n> +       g_free(c->password_expiry_utc);\n>\n>         credential_init(c);\n>  }\n> @@ -285,6 +311,9 @@ static int credential_read(struct credential *c)\n>                 } else if (!strcmp(key, \"username\")) {\n>                         g_free(c->username);\n>                         c->username = g_strdup(value);\n> +               } else if (!strcmp(key, \"password_expiry_utc\")) {\n> +                       g_free(c->password_expiry_utc);\n> +                       c->password_expiry_utc = g_strdup(value);\n>                 } else if (!strcmp(key, \"password\")) {\n>                         g_free(c->password);\n>                         c->password = g_strdup(value);\n> @@ -312,9 +341,11 @@ static void credential_write_item(FILE *fp, const char *key, const char *value)\n>\n>  static void credential_write(const struct credential *c)\n>  {\n> -       /* only write username/password, if set */\n> +       /* only write username/password/expiry, if set */\n>         credential_write_item(stdout, \"username\", c->username);\n>         credential_write_item(stdout, \"password\", c->password);\n> +       credential_write_item(stdout, \"password_expiry_utc\",\n> +               c->password_expiry_utc);\n>  }\n>\n>  static void usage(const char *name)\n> diff --git a/t/lib-credential.sh b/t/lib-credential.sh\n> index 5ea8bc9f1dc..9ebf7eeae48 100644\n> --- a/t/lib-credential.sh\n> +++ b/t/lib-credential.sh\n> @@ -43,6 +43,7 @@ helper_test_clean() {\n>         reject $1 https example.com store-user\n>         reject $1 https example.com user1\n>         reject $1 https example.com user2\n> +       reject $1 https example.com user3\n>         reject $1 http path.tld user\n>         reject $1 https timeout.tld user\n>         reject $1 https sso.tld\n> @@ -298,6 +299,35 @@ helper_test_timeout() {\n>         '\n>  }\n>\n> +helper_test_password_expiry_utc() {\n> +       HELPER=$1\n> +\n> +       test_expect_success \"helper ($HELPER) stores password_expiry_utc\" '\n> +               check approve $HELPER <<-\\EOF\n> +               protocol=https\n> +               host=example.com\n> +               username=user3\n> +               password=pass\n> +               password_expiry_utc=9999999999\n> +               EOF\n> +       '\n> +\n> +       test_expect_success \"helper ($HELPER) gets password_expiry_utc\" '\n> +               check fill $HELPER <<-\\EOF\n> +               protocol=https\n> +               host=example.com\n> +               username=user3\n> +               --\n> +               protocol=https\n> +               host=example.com\n> +               username=user3\n> +               password=pass\n> +               password_expiry_utc=9999999999\n> +               --\n> +               EOF\n> +       '\n> +}\n> +\n>  write_script askpass <<\\EOF\n>  echo >&2 askpass: $*\n>  what=$(echo $1 | cut -d\" \" -f1 | tr A-Z a-z | tr -cd a-z)\n> diff --git a/t/t0301-credential-cache.sh b/t/t0301-credential-cache.sh\n> index 698b7159f03..f5ba727e53b 100755\n> --- a/t/t0301-credential-cache.sh\n> +++ b/t/t0301-credential-cache.sh\n> @@ -30,6 +30,8 @@ test_atexit 'git credential-cache exit'\n>  # test that the daemon works with no special setup\n>  helper_test cache\n>\n> +helper_test_password_expiry_utc cache\n> +\n>  test_expect_success 'socket defaults to ~/.cache/git/credential/socket' '\n>         test_when_finished \"\n>                 git credential-cache exit &&\n> diff --git a/t/t0303-credential-external.sh b/t/t0303-credential-external.sh\n> index f028fd14182..f1478680bff 100755\n> --- a/t/t0303-credential-external.sh\n> +++ b/t/t0303-credential-external.sh\n> @@ -52,6 +52,8 @@ else\n>         helper_test_timeout \"$GIT_TEST_CREDENTIAL_HELPER_TIMEOUT\"\n>  fi\n>\n> +helper_test_password_expiry_utc \"$GIT_TEST_CREDENTIAL_HELPER\"\n> +\n>  # clean afterwards so that we are good citizens\n>  # and don't leave cruft in the helper's storage, which\n>  # might be long-term system storage\n>\n> base-commit: 27d43aaaf50ef0ae014b88bba294f93658016a2e\n> --\n> gitgitgadget\n"},{"id":"477291","messageId":"xmqq353xa2cg.fsf@gitster.g","threadId":"59398","inReplyTo":"CAGJzqskMwOJkriH6serqdwAVYi+fftEL8ohJd-suP6v+OxB_bg@mail.gmail.com","subject":"Re: [PATCH v3] credential/libsecret: support password_expiry_utc","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-05-15T18:14:55Z","receivedAt":"2023-05-15T18:17:12Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"M Hickford <mirth.hickford@gmail.com> writes:\n\n>> +static const SecretSchema schema = {\n>> +       \"org.git.Password\",\n>> +       /* Ignore schema name for backwards compatibility with previous versions */\n>> +       SECRET_SCHEMA_DONT_MATCH_NAME,\n>> +       {\n>> +               {  \"user\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n>> +               {  \"object\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n>> +               {  \"protocol\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n>> +               {  \"port\", SECRET_SCHEMA_ATTRIBUTE_INTEGER },\n>> +               {  \"server\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n>> +               {  \"password_expiry_utc\", SECRET_SCHEMA_ATTRIBUTE_INTEGER },\n>\n> I've been testing this patch with credential-generating helper\n> git-credential-helper. It works, but because libsecret overwrites\n> items if and only if the attributes match exactly, you end up with\n> many items in the secret store that differ only by expiry date. This\n> is inelegant, and confusing to users. Please hold this patch, don't\n> merge to master. A solution might be to store the expiry date as the\n> secret of a separate item (even though the value is not confidential)\n\nThanks for stopping me.  I'll mark the topic as \"on hold\".\n\nIt does sound problematic, but if we think about what is used as\nkeys and what is used as values, it does make a lot more sense to\nstore the expiry as part of a value.  After all, we are not even\nasking \"give me the password that will expire in the most distant\nfuture\" or anything like that.  We consult the database with \"who\nwants to access what server over which protocol at what port\" as the\nkey and expect we find the suitable authentication material to use.\nIt would be best if we can treat the expiry date as an additional\nattribute of that authentication material.  \n\nDo the methods to store and retrieve a password from the keyring\nallow us to add such an extra attribute to the password?  I have no\nidea how the Gnome keyring API works, but is there a way to mark\neach entry in the SecretSchemaAttributes as \"this is used as a key\"\nvs \"this is used as a value---do not match\"?  Would thinking along\nsuch a line help?\n\nAnother possibility would be to store encoded concatenation of the\nreal password and expiration timestamp and decode them into two upon\nretrieval.  If we were the only user of the keystore, that may work,\nbut if we are sharing the keystore with other applications, it would\nbe a non-starter.\n\nWhat do other application do, when using the keyring to store\nexpirable passwords with services that do let you know the\nexpiration time of the password?  If they just ask the users again\nonly after finding out that the password did not work, perhaps we\nshould do the same, without being proactive and notice the expiry\nourselves?  That is, instead of failing the access to the server\nimmediately upon seeing an auth failure, if the authentication\nmaterial is know to have expiration time, can we let the application\nlayer to ask the end-user to provide an refreshed password and try\nagain?  For such a scheme, we do not have to store ever-changing\n\"password_expiry_utc\" and contaminate the keyring with crufts whose\nexpiry dates are the only difference.  Instead we can just have a\nBoolean \"does this site expire a valid password?\" and use it to\nbehave differently, if desired, from sites for which the passwords\ndo not expire, perhaps?\n\n"},{"id":"477379","messageId":"CAGJzqsn_Jmr3x0tj3U7QRD3eGMzWwr3RR1-O3_YzFCMCU7u+eQ@mail.gmail.com","threadId":"59398","inReplyTo":"xmqq353xa2cg.fsf@gitster.g","subject":"Re: [PATCH v3] credential/libsecret: support password_expiry_utc","fromName":"M Hickford","fromEmail":"mirth.hickford@gmail.com","sentAt":"2023-05-16T08:03:51Z","receivedAt":"2023-05-16T08:04:38Z","isPatch":true,"sender":{"key":"mirth.hickford@gmail.com","avatar":"https://avatars.githubusercontent.com/u/105314?v=4"},"body":"On Mon, 15 May 2023 at 19:15, Junio C Hamano <gitster@pobox.com> wrote:\n>\n> M Hickford <mirth.hickford@gmail.com> writes:\n>\n> >> +static const SecretSchema schema = {\n> >> +       \"org.git.Password\",\n> >> +       /* Ignore schema name for backwards compatibility with previous versions */\n> >> +       SECRET_SCHEMA_DONT_MATCH_NAME,\n> >> +       {\n> >> +               {  \"user\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n> >> +               {  \"object\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n> >> +               {  \"protocol\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n> >> +               {  \"port\", SECRET_SCHEMA_ATTRIBUTE_INTEGER },\n> >> +               {  \"server\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n> >> +               {  \"password_expiry_utc\", SECRET_SCHEMA_ATTRIBUTE_INTEGER },\n> >\n> > I've been testing this patch with credential-generating helper\n> > git-credential-helper. It works, but because libsecret overwrites\n> > items if and only if the attributes match exactly, you end up with\n> > many items in the secret store that differ only by expiry date. This\n> > is inelegant, and confusing to users. Please hold this patch, don't\n> > merge to master. A solution might be to store the expiry date as the\n> > secret of a separate item (even though the value is not confidential)\n>\n> Thanks for stopping me.  I'll mark the topic as \"on hold\".\n\nThanks Junio\n\n>\n> It does sound problematic, but if we think about what is used as\n> keys and what is used as values, it does make a lot more sense to\n> store the expiry as part of a value.  After all, we are not even\n> asking \"give me the password that will expire in the most distant\n> future\" or anything like that.  We consult the database with \"who\n> wants to access what server over which protocol at what port\" as the\n> key and expect we find the suitable authentication material to use.\n> It would be best if we can treat the expiry date as an additional\n> attribute of that authentication material.\n>\n> Do the methods to store and retrieve a password from the keyring\n> allow us to add such an extra attribute to the password?  I have no\n> idea how the Gnome keyring API works, but is there a way to mark\n> each entry in the SecretSchemaAttributes as \"this is used as a key\"\n> vs \"this is used as a value---do not match\"?  Would thinking along\n> such a line help?\n\nUnfortunately not. The libsecret docs warn \"Attributes are meant to be\nused for lookup of items; they’re not designed to be used as a generic\nkey/value database\".\nhttps://gnome.pages.gitlab.gnome.org/libsecret/migrating-libgnome-keyring.html\n\nInterestingly, Windows' wincred API doesn't have this issue, because\nit searches by a unique key, defined separately to attributes.\n\n>\n> Another possibility would be to store encoded concatenation of the\n> real password and expiration timestamp and decode them into two upon\n> retrieval.  If we were the only user of the keystore, that may work,\n> but if we are sharing the keystore with other applications, it would\n> be a non-starter.\n\nYes, I think that would work nicely. A format such as that below would\nbe backwards compatible (passwords already can't contain newlines) and\nself explanatory to any curious user browsing their secret store. I\nalready have a draft that works much like this. I'll prepare a patch\nv4.\n\n    7d7b554\n    password_expiry_utc=1684179877\n    oauth_refresh_token=be8a9aa3\n\nIs the secret store ever shared with other applications such as a web\nbrowser? If so, sharing is already broken, because popular Git hosts\nsuch as GitHub and GitLab expect different passwords for web login and\nGit authentication (OAuth token or personal access token). A solution\ncould be to introduce our own libsecret schema (as in the current\npatch) instead of continuing to use SECRET_SCHEMA_COMPAT_NETWORK\npotentially shared with other apps. I'm not sure whether that's\nworthwhile in this patch. I defer to you.\n\nAFAIK major web browsers all implement their own password store rather\nthan use libsecret, though apparently Chromium has a build flag to use\nlibsecret on Linux.\nhttps://source.chromium.org/search?q=libsecret%20-f:third_party&ss=chromium\n\n>\n> What do other application do, when using the keyring to store\n> expirable passwords with services that do let you know the\n> expiration time of the password?  If they just ask the users again\n> only after finding out that the password did not work, perhaps we\n> should do the same, without being proactive and notice the expiry\n> ourselves?  That is, instead of failing the access to the server\n> immediately upon seeing an auth failure, if the authentication\n> material is know to have expiration time, can we let the application\n> layer to ask the end-user to provide an refreshed password and try\n> again?  For such a scheme, we do not have to store ever-changing\n> \"password_expiry_utc\" and contaminate the keyring with crufts whose\n> expiry dates are the only difference.  Instead we can just have a\n> Boolean \"does this site expire a valid password?\" and use it to\n> behave differently, if desired, from sites for which the passwords\n> do not expire, perhaps?\n>\n\nIn the case of HTTP error 401 Unauthorized, Git calls `credential\nreject` then exits. An improvement could be to first call `credential\nfill` again (this time without password prompt) and retry if a new\ncredential is returned. A fresh credential if generated is likely to\nbe valid. This would improve the experience when using a\ncredential-generating helper together with a storage helper that drops\npassword_expiry_utc.\n\nBest remains to use a storage helper that stores password_expiry_utc.\nThis avoids a doomed HTTP request. Storing oauth_refresh_token is also\nbeneficial, because OAuth refresh is faster than cold OAuth.\n"},{"id":"477384","messageId":"xmqq4jocw93a.fsf@gitster.g","threadId":"59398","inReplyTo":"CAGJzqsn_Jmr3x0tj3U7QRD3eGMzWwr3RR1-O3_YzFCMCU7u+eQ@mail.gmail.com","subject":"Re: [PATCH v3] credential/libsecret: support password_expiry_utc","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-05-16T16:10:33Z","receivedAt":"2023-05-16T16:12:22Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"M Hickford <mirth.hickford@gmail.com> writes:\n\n> Yes, I think that would work nicely. A format such as that below would\n> be backwards compatible (passwords already can't contain newlines) and\n> self explanatory to any curious user browsing their secret store. I\n> already have a draft that works much like this. I'll prepare a patch\n> v4.\n>\n>     7d7b554\n>     password_expiry_utc=1684179877\n>     oauth_refresh_token=be8a9aa3\n>\n> Is the secret store ever shared with other applications such as a web\n> browser? If so, sharing is already broken, because popular Git hosts\n> such as GitHub and GitLab expect different passwords for web login and\n> Git authentication (OAuth token or personal access token).\n\nIt probably is a good argument.  We do not have to worry about\ninteroperating with browsers and their authentication with Git\nhosting sites.  And the \"newline can be used to add extra pieces of\ninformation\" is a good trick ;-)\n\n> A solution\n> could be to introduce our own libsecret schema (as in the current\n> patch) instead of continuing to use SECRET_SCHEMA_COMPAT_NETWORK\n> potentially shared with other apps. I'm not sure whether that's\n> worthwhile in this patch. I defer to you.\n\nIt may depend on what other Git GUI frontends may want to do.  If\nthere is no precedent and you are the pioneer, then using our own\nwould be perfectly fine and we can let others also read from us if\nthey want to; I presume that it would not prevent us from doing so\neven if did not use COMPAT_NETWORK (which gnome dev document even\ndiscourages of its use in new applications anyway).\n\nThanks.\n\n\n"},{"id":"477428","messageId":"pull.1469.v4.git.git.1684306540947.gitgitgadget@gmail.com","threadId":"59398","inReplyTo":"pull.1469.v3.git.git.1683270298313.gitgitgadget@gmail.com","subject":"[PATCH v4] credential/libsecret: store new attributes","fromName":"M Hickford via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2023-05-17T06:55:40Z","receivedAt":"2023-05-17T06:55:52Z","isPatch":true,"sender":{"key":"mirth.hickford@gmail.com","avatar":"https://avatars.githubusercontent.com/u/105314?v=4"},"body":"From: M Hickford <mirth.hickford@gmail.com>\n\nd208bfd (credential: new attribute password_expiry_utc, 2023-02-18)\nand a5c76569e7 (credential: new attribute oauth_refresh_token)\nintroduced new credential attributes.\n\nlibsecret assumes attribute values are non-confidential and\nunchanging, so we encode the new attributes in the secret, separated by\nnewline:\n\n    hunter2\n    password_expiry_utc=1684189401\n    oauth_refresh_token=xyzzy\n\nThis is extensible and backwards compatible. The credential protocol\nalready assumes that attribute values do not contain newlines.\n\nAlternatives considered: store password_expiry_utc in a libsecret\nattribute. This has the problem that libsecret creates new items\nrather than overwrites when attribute values change.\n\nSigned-off-by: M Hickford <mirth.hickford@gmail.com>\n---\n    credential/libsecret: store password_expiry_utc and oauth_refresh_token\n    \n    Patch v4 stores password_expiry_utc inside secret instead of in an\n    attribute.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1469%2Fhickford%2Flibsecret-v4\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1469/hickford/libsecret-v4\nPull-Request: https://github.com/git/git/pull/1469\n\nRange-diff vs v3:\n\n 1:  b46594c8897 ! 1:  07452a10372 credential/libsecret: support password_expiry_utc\n     @@ Metadata\n      Author: M Hickford <mirth.hickford@gmail.com>\n      \n       ## Commit message ##\n     -    credential/libsecret: support password_expiry_utc\n     +    credential/libsecret: store new attributes\n      \n          d208bfd (credential: new attribute password_expiry_utc, 2023-02-18)\n     -    introduced this attribute.\n     +    and a5c76569e7 (credential: new attribute oauth_refresh_token)\n     +    introduced new credential attributes.\n     +\n     +    libsecret assumes attribute values are non-confidential and\n     +    unchanging, so we encode the new attributes in the secret, separated by\n     +    newline:\n     +\n     +        hunter2\n     +        password_expiry_utc=1684189401\n     +        oauth_refresh_token=xyzzy\n     +\n     +    This is extensible and backwards compatible. The credential protocol\n     +    already assumes that attribute values do not contain newlines.\n     +\n     +    Alternatives considered: store password_expiry_utc in a libsecret\n     +    attribute. This has the problem that libsecret creates new items\n     +    rather than overwrites when attribute values change.\n      \n          Signed-off-by: M Hickford <mirth.hickford@gmail.com>\n      \n     @@ contrib/credential/libsecret/git-credential-libsecret.c: struct credential {\n       \tchar *username;\n       \tchar *password;\n      +\tchar *password_expiry_utc;\n     ++\tchar *oauth_refresh_token;\n       };\n       \n       #define CREDENTIAL_INIT { 0 }\n     @@ contrib/credential/libsecret/git-credential-libsecret.c: struct credential_opera\n       \n      +static const SecretSchema schema = {\n      +\t\"org.git.Password\",\n     -+\t/* Ignore schema name for backwards compatibility with previous versions */\n     ++\t/* Ignore schema name during search for backwards compatibility */\n      +\tSECRET_SCHEMA_DONT_MATCH_NAME,\n      +\t{\n     ++\t\t/*\n     ++\t\t * libsecret assumes attribute values are non-confidential and\n     ++\t\t * unchanging, so we can't include oauth_refresh_token or\n     ++\t\t * password_expiry_utc.\n     ++\t\t */\n      +\t\t{  \"user\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n      +\t\t{  \"object\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n      +\t\t{  \"protocol\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n      +\t\t{  \"port\", SECRET_SCHEMA_ATTRIBUTE_INTEGER },\n      +\t\t{  \"server\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n     -+\t\t{  \"password_expiry_utc\", SECRET_SCHEMA_ATTRIBUTE_INTEGER },\n      +\t\t{  NULL, 0 },\n      +\t}\n      +};\n     @@ contrib/credential/libsecret/git-credential-libsecret.c: struct credential_opera\n       static char *make_label(struct credential *c)\n       {\n       \tif (c->port)\n     -@@ contrib/credential/libsecret/git-credential-libsecret.c: static GHashTable *make_attr_list(struct credential *c)\n     - \t\tg_hash_table_insert(al, \"port\", g_strdup_printf(\"%hu\", c->port));\n     - \tif (c->path)\n     - \t\tg_hash_table_insert(al, \"object\", g_strdup(c->path));\n     -+\tif (c->password_expiry_utc)\n     -+\t\tg_hash_table_insert(al, \"password_expiry_utc\",\n     -+\t\t\tg_strdup(c->password_expiry_utc));\n     - \n     - \treturn al;\n     - }\n      @@ contrib/credential/libsecret/git-credential-libsecret.c: static int keyring_get(struct credential *c)\n       \n       \tattributes = make_attr_list(c);\n     @@ contrib/credential/libsecret/git-credential-libsecret.c: static int keyring_get(\n       \t\t\t\t\t   SECRET_SEARCH_LOAD_SECRETS | SECRET_SEARCH_UNLOCK,\n       \t\t\t\t\t   NULL,\n      @@ contrib/credential/libsecret/git-credential-libsecret.c: static int keyring_get(struct credential *c)\n     - \t\t\tc->username = g_strdup(s);\n     - \t\t}\n     + \t\tSecretItem *item;\n     + \t\tSecretValue *secret;\n     + \t\tconst char *s;\n     ++\t\tgchar **parts;\n     + \n     + \t\titem = items->data;\n     + \t\tsecret = secret_item_get_secret(item);\n     +@@ contrib/credential/libsecret/git-credential-libsecret.c: static int keyring_get(struct credential *c)\n       \n     -+\t\ts = g_hash_table_lookup(attributes, \"password_expiry_utc\");\n     -+\t\tif (s) {\n     -+\t\t\tg_free(c->password_expiry_utc);\n     -+\t\t\tc->password_expiry_utc = g_strdup(s);\n     -+\t\t}\n     -+\n       \t\ts = secret_value_get_text(secret);\n       \t\tif (s) {\n     - \t\t\tg_free(c->password);\n     +-\t\t\tg_free(c->password);\n     +-\t\t\tc->password = g_strdup(s);\n     ++\t\t\t/*\n     ++\t\t\t * Passwords and other attributes encoded in following format:\n     ++\t\t\t *   hunter2\n     ++\t\t\t *   password_expiry_utc=1684189401\n     ++\t\t\t *   oauth_refresh_token=xyzzy\n     ++\t\t\t */\n     ++\t\t\tparts = g_strsplit(s, \"\\n\", 0);\n     ++\t\t\tif (g_strv_length(parts) >= 1) {\n     ++\t\t\t\tg_free(c->password);\n     ++\t\t\t\tc->password = g_strdup(parts[0]);\n     ++\t\t\t}\n     ++\t\t\tfor (int i = 1; i < g_strv_length(parts); i++) {\n     ++\t\t\t\tif (g_str_has_prefix(parts[i], \"password_expiry_utc=\")) {\n     ++\t\t\t\t\tg_free(c->password_expiry_utc);\n     ++\t\t\t\t\tc->password_expiry_utc = g_strdup(&parts[i][20]);\n     ++\t\t\t\t} else if (g_str_has_prefix(parts[i], \"oauth_refresh_token=\")) {\n     ++\t\t\t\t\tg_free(c->oauth_refresh_token);\n     ++\t\t\t\t\tc->oauth_refresh_token = g_strdup(&parts[i][20]);\n     ++\t\t\t\t}\n     ++\t\t\t}\n     ++\t\t\tg_strfreev(parts);\n     + \t\t}\n     + \n     + \t\tg_hash_table_unref(attributes);\n     +@@ contrib/credential/libsecret/git-credential-libsecret.c: static int keyring_store(struct credential *c)\n     + \tchar *label = NULL;\n     + \tGHashTable *attributes = NULL;\n     + \tGError *error = NULL;\n     ++\tGString *secret = NULL;\n     + \n     + \t/*\n     + \t * Sanity check that what we are storing is actually sensible.\n      @@ contrib/credential/libsecret/git-credential-libsecret.c: static int keyring_store(struct credential *c)\n       \n       \tlabel = make_label(c);\n       \tattributes = make_attr_list(c);\n      -\tsecret_password_storev_sync(SECRET_SCHEMA_COMPAT_NETWORK,\n     ++\tsecret = g_string_new(c->password);\n     ++\tif (c->password_expiry_utc) {\n     ++\t\tg_string_append_printf(secret, \"\\npassword_expiry_utc=%s\",\n     ++\t\t\tc->password_expiry_utc);\n     ++\t}\n     ++\tif (c->oauth_refresh_token) {\n     ++\t\tg_string_append_printf(secret, \"\\noauth_refresh_token=%s\",\n     ++\t\t\tc->oauth_refresh_token);\n     ++\t}\n      +\tsecret_password_storev_sync(&schema,\n       \t\t\t\t    attributes,\n       \t\t\t\t    NULL,\n       \t\t\t\t    label,\n     +-\t\t\t\t    c->password,\n     ++\t\t\t\t    secret->str,\n     + \t\t\t\t    NULL,\n     + \t\t\t\t    &error);\n     ++\tg_string_free(secret, TRUE);\n     + \tg_free(label);\n     + \tg_hash_table_unref(attributes);\n     + \n      @@ contrib/credential/libsecret/git-credential-libsecret.c: static int keyring_erase(struct credential *c)\n       \t\treturn EXIT_FAILURE;\n       \n     @@ contrib/credential/libsecret/git-credential-libsecret.c: static void credential_\n       \tg_free(c->username);\n       \tg_free(c->password);\n      +\tg_free(c->password_expiry_utc);\n     ++\tg_free(c->oauth_refresh_token);\n       \n       \tcredential_init(c);\n       }\n     @@ contrib/credential/libsecret/git-credential-libsecret.c: static int credential_r\n       \t\t} else if (!strcmp(key, \"password\")) {\n       \t\t\tg_free(c->password);\n       \t\t\tc->password = g_strdup(value);\n     -@@ contrib/credential/libsecret/git-credential-libsecret.c: static void credential_write_item(FILE *fp, const char *key, const char *value)\n     - \n     - static void credential_write(const struct credential *c)\n     - {\n     --\t/* only write username/password, if set */\n     -+\t/* only write username/password/expiry, if set */\n     + \t\t\twhile (*value)\n     + \t\t\t\t*value++ = '\\0';\n     ++\t\t} else if (!strcmp(key, \"oauth_refresh_token\")) {\n     ++\t\t\tg_free(c->oauth_refresh_token);\n     ++\t\t\tc->oauth_refresh_token = g_strdup(value);\n     ++\t\t\twhile (*value)\n     ++\t\t\t\t*value++ = '\\0';\n     + \t\t}\n     + \t\t/*\n     + \t\t * Ignore other lines; we don't know what they mean, but\n     +@@ contrib/credential/libsecret/git-credential-libsecret.c: static void credential_write(const struct credential *c)\n     + \t/* only write username/password, if set */\n       \tcredential_write_item(stdout, \"username\", c->username);\n       \tcredential_write_item(stdout, \"password\", c->password);\n      +\tcredential_write_item(stdout, \"password_expiry_utc\",\n      +\t\tc->password_expiry_utc);\n     ++\tcredential_write_item(stdout, \"oauth_refresh_token\",\n     ++\t\tc->oauth_refresh_token);\n       }\n       \n       static void usage(const char *name)\n     @@ t/lib-credential.sh: helper_test_clean() {\n       \treject $1 https example.com user1\n       \treject $1 https example.com user2\n      +\treject $1 https example.com user3\n     + \treject $1 https example.com user4\n       \treject $1 http path.tld user\n       \treject $1 https timeout.tld user\n     - \treject $1 https sso.tld\n      @@ t/lib-credential.sh: helper_test_timeout() {\n       \t'\n       }\n     @@ t/lib-credential.sh: helper_test_timeout() {\n      +\t'\n      +}\n      +\n     - write_script askpass <<\\EOF\n     - echo >&2 askpass: $*\n     - what=$(echo $1 | cut -d\" \" -f1 | tr A-Z a-z | tr -cd a-z)\n     + helper_test_oauth_refresh_token() {\n     + \tHELPER=$1\n     + \n      \n       ## t/t0301-credential-cache.sh ##\n      @@ t/t0301-credential-cache.sh: test_atexit 'git credential-cache exit'\n     + \n       # test that the daemon works with no special setup\n       helper_test cache\n     - \n      +helper_test_password_expiry_utc cache\n     -+\n     + helper_test_oauth_refresh_token cache\n     + \n       test_expect_success 'socket defaults to ~/.cache/git/credential/socket' '\n     - \ttest_when_finished \"\n     - \t\tgit credential-cache exit &&\n      \n       ## t/t0303-credential-external.sh ##\n     -@@ t/t0303-credential-external.sh: else\n     - \thelper_test_timeout \"$GIT_TEST_CREDENTIAL_HELPER_TIMEOUT\"\n     - fi\n     +@@ t/t0303-credential-external.sh: test -z \"$GIT_TEST_CREDENTIAL_HELPER_SETUP\" ||\n     + helper_test_clean \"$GIT_TEST_CREDENTIAL_HELPER\"\n       \n     + helper_test \"$GIT_TEST_CREDENTIAL_HELPER\"\n      +helper_test_password_expiry_utc \"$GIT_TEST_CREDENTIAL_HELPER\"\n     -+\n     - # clean afterwards so that we are good citizens\n     - # and don't leave cruft in the helper's storage, which\n     - # might be long-term system storage\n     ++helper_test_oauth_refresh_token \"$GIT_TEST_CREDENTIAL_HELPER\"\n     + \n     + if test -z \"$GIT_TEST_CREDENTIAL_HELPER_TIMEOUT\"; then\n     + \tsay \"# skipping timeout tests (GIT_TEST_CREDENTIAL_HELPER_TIMEOUT not set)\"\n\n\n .../libsecret/git-credential-libsecret.c      | 78 +++++++++++++++++--\n t/lib-credential.sh                           | 30 +++++++\n t/t0301-credential-cache.sh                   |  1 +\n t/t0303-credential-external.sh                |  2 +\n 4 files changed, 105 insertions(+), 6 deletions(-)\n\ndiff --git a/contrib/credential/libsecret/git-credential-libsecret.c b/contrib/credential/libsecret/git-credential-libsecret.c\nindex ef681f29d5b..31cf32ad969 100644\n--- a/contrib/credential/libsecret/git-credential-libsecret.c\n+++ b/contrib/credential/libsecret/git-credential-libsecret.c\n@@ -39,6 +39,8 @@ struct credential {\n \tchar *path;\n \tchar *username;\n \tchar *password;\n+\tchar *password_expiry_utc;\n+\tchar *oauth_refresh_token;\n };\n \n #define CREDENTIAL_INIT { 0 }\n@@ -54,6 +56,25 @@ struct credential_operation {\n \n /* ----------------- Secret Service functions ----------------- */\n \n+static const SecretSchema schema = {\n+\t\"org.git.Password\",\n+\t/* Ignore schema name during search for backwards compatibility */\n+\tSECRET_SCHEMA_DONT_MATCH_NAME,\n+\t{\n+\t\t/*\n+\t\t * libsecret assumes attribute values are non-confidential and\n+\t\t * unchanging, so we can't include oauth_refresh_token or\n+\t\t * password_expiry_utc.\n+\t\t */\n+\t\t{  \"user\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n+\t\t{  \"object\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n+\t\t{  \"protocol\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n+\t\t{  \"port\", SECRET_SCHEMA_ATTRIBUTE_INTEGER },\n+\t\t{  \"server\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n+\t\t{  NULL, 0 },\n+\t}\n+};\n+\n static char *make_label(struct credential *c)\n {\n \tif (c->port)\n@@ -101,7 +122,7 @@ static int keyring_get(struct credential *c)\n \n \tattributes = make_attr_list(c);\n \titems = secret_service_search_sync(service,\n-\t\t\t\t\t   SECRET_SCHEMA_COMPAT_NETWORK,\n+\t\t\t\t\t   &schema,\n \t\t\t\t\t   attributes,\n \t\t\t\t\t   SECRET_SEARCH_LOAD_SECRETS | SECRET_SEARCH_UNLOCK,\n \t\t\t\t\t   NULL,\n@@ -117,6 +138,7 @@ static int keyring_get(struct credential *c)\n \t\tSecretItem *item;\n \t\tSecretValue *secret;\n \t\tconst char *s;\n+\t\tgchar **parts;\n \n \t\titem = items->data;\n \t\tsecret = secret_item_get_secret(item);\n@@ -130,8 +152,27 @@ static int keyring_get(struct credential *c)\n \n \t\ts = secret_value_get_text(secret);\n \t\tif (s) {\n-\t\t\tg_free(c->password);\n-\t\t\tc->password = g_strdup(s);\n+\t\t\t/*\n+\t\t\t * Passwords and other attributes encoded in following format:\n+\t\t\t *   hunter2\n+\t\t\t *   password_expiry_utc=1684189401\n+\t\t\t *   oauth_refresh_token=xyzzy\n+\t\t\t */\n+\t\t\tparts = g_strsplit(s, \"\\n\", 0);\n+\t\t\tif (g_strv_length(parts) >= 1) {\n+\t\t\t\tg_free(c->password);\n+\t\t\t\tc->password = g_strdup(parts[0]);\n+\t\t\t}\n+\t\t\tfor (int i = 1; i < g_strv_length(parts); i++) {\n+\t\t\t\tif (g_str_has_prefix(parts[i], \"password_expiry_utc=\")) {\n+\t\t\t\t\tg_free(c->password_expiry_utc);\n+\t\t\t\t\tc->password_expiry_utc = g_strdup(&parts[i][20]);\n+\t\t\t\t} else if (g_str_has_prefix(parts[i], \"oauth_refresh_token=\")) {\n+\t\t\t\t\tg_free(c->oauth_refresh_token);\n+\t\t\t\t\tc->oauth_refresh_token = g_strdup(&parts[i][20]);\n+\t\t\t\t}\n+\t\t\t}\n+\t\t\tg_strfreev(parts);\n \t\t}\n \n \t\tg_hash_table_unref(attributes);\n@@ -148,6 +189,7 @@ static int keyring_store(struct credential *c)\n \tchar *label = NULL;\n \tGHashTable *attributes = NULL;\n \tGError *error = NULL;\n+\tGString *secret = NULL;\n \n \t/*\n \t * Sanity check that what we are storing is actually sensible.\n@@ -162,13 +204,23 @@ static int keyring_store(struct credential *c)\n \n \tlabel = make_label(c);\n \tattributes = make_attr_list(c);\n-\tsecret_password_storev_sync(SECRET_SCHEMA_COMPAT_NETWORK,\n+\tsecret = g_string_new(c->password);\n+\tif (c->password_expiry_utc) {\n+\t\tg_string_append_printf(secret, \"\\npassword_expiry_utc=%s\",\n+\t\t\tc->password_expiry_utc);\n+\t}\n+\tif (c->oauth_refresh_token) {\n+\t\tg_string_append_printf(secret, \"\\noauth_refresh_token=%s\",\n+\t\t\tc->oauth_refresh_token);\n+\t}\n+\tsecret_password_storev_sync(&schema,\n \t\t\t\t    attributes,\n \t\t\t\t    NULL,\n \t\t\t\t    label,\n-\t\t\t\t    c->password,\n+\t\t\t\t    secret->str,\n \t\t\t\t    NULL,\n \t\t\t\t    &error);\n+\tg_string_free(secret, TRUE);\n \tg_free(label);\n \tg_hash_table_unref(attributes);\n \n@@ -198,7 +250,7 @@ static int keyring_erase(struct credential *c)\n \t\treturn EXIT_FAILURE;\n \n \tattributes = make_attr_list(c);\n-\tsecret_password_clearv_sync(SECRET_SCHEMA_COMPAT_NETWORK,\n+\tsecret_password_clearv_sync(&schema,\n \t\t\t\t    attributes,\n \t\t\t\t    NULL,\n \t\t\t\t    &error);\n@@ -238,6 +290,8 @@ static void credential_clear(struct credential *c)\n \tg_free(c->path);\n \tg_free(c->username);\n \tg_free(c->password);\n+\tg_free(c->password_expiry_utc);\n+\tg_free(c->oauth_refresh_token);\n \n \tcredential_init(c);\n }\n@@ -284,11 +338,19 @@ static int credential_read(struct credential *c)\n \t\t} else if (!strcmp(key, \"username\")) {\n \t\t\tg_free(c->username);\n \t\t\tc->username = g_strdup(value);\n+\t\t} else if (!strcmp(key, \"password_expiry_utc\")) {\n+\t\t\tg_free(c->password_expiry_utc);\n+\t\t\tc->password_expiry_utc = g_strdup(value);\n \t\t} else if (!strcmp(key, \"password\")) {\n \t\t\tg_free(c->password);\n \t\t\tc->password = g_strdup(value);\n \t\t\twhile (*value)\n \t\t\t\t*value++ = '\\0';\n+\t\t} else if (!strcmp(key, \"oauth_refresh_token\")) {\n+\t\t\tg_free(c->oauth_refresh_token);\n+\t\t\tc->oauth_refresh_token = g_strdup(value);\n+\t\t\twhile (*value)\n+\t\t\t\t*value++ = '\\0';\n \t\t}\n \t\t/*\n \t\t * Ignore other lines; we don't know what they mean, but\n@@ -314,6 +376,10 @@ static void credential_write(const struct credential *c)\n \t/* only write username/password, if set */\n \tcredential_write_item(stdout, \"username\", c->username);\n \tcredential_write_item(stdout, \"password\", c->password);\n+\tcredential_write_item(stdout, \"password_expiry_utc\",\n+\t\tc->password_expiry_utc);\n+\tcredential_write_item(stdout, \"oauth_refresh_token\",\n+\t\tc->oauth_refresh_token);\n }\n \n static void usage(const char *name)\ndiff --git a/t/lib-credential.sh b/t/lib-credential.sh\nindex f1ab92ba35c..4ad498adbc4 100644\n--- a/t/lib-credential.sh\n+++ b/t/lib-credential.sh\n@@ -43,6 +43,7 @@ helper_test_clean() {\n \treject $1 https example.com store-user\n \treject $1 https example.com user1\n \treject $1 https example.com user2\n+\treject $1 https example.com user3\n \treject $1 https example.com user4\n \treject $1 http path.tld user\n \treject $1 https timeout.tld user\n@@ -328,6 +329,35 @@ helper_test_timeout() {\n \t'\n }\n \n+helper_test_password_expiry_utc() {\n+\tHELPER=$1\n+\n+\ttest_expect_success \"helper ($HELPER) stores password_expiry_utc\" '\n+\t\tcheck approve $HELPER <<-\\EOF\n+\t\tprotocol=https\n+\t\thost=example.com\n+\t\tusername=user3\n+\t\tpassword=pass\n+\t\tpassword_expiry_utc=9999999999\n+\t\tEOF\n+\t'\n+\n+\ttest_expect_success \"helper ($HELPER) gets password_expiry_utc\" '\n+\t\tcheck fill $HELPER <<-\\EOF\n+\t\tprotocol=https\n+\t\thost=example.com\n+\t\tusername=user3\n+\t\t--\n+\t\tprotocol=https\n+\t\thost=example.com\n+\t\tusername=user3\n+\t\tpassword=pass\n+\t\tpassword_expiry_utc=9999999999\n+\t\t--\n+\t\tEOF\n+\t'\n+}\n+\n helper_test_oauth_refresh_token() {\n \tHELPER=$1\n \ndiff --git a/t/t0301-credential-cache.sh b/t/t0301-credential-cache.sh\nindex c02a3b5969c..8300faadea9 100755\n--- a/t/t0301-credential-cache.sh\n+++ b/t/t0301-credential-cache.sh\n@@ -29,6 +29,7 @@ test_atexit 'git credential-cache exit'\n \n # test that the daemon works with no special setup\n helper_test cache\n+helper_test_password_expiry_utc cache\n helper_test_oauth_refresh_token cache\n \n test_expect_success 'socket defaults to ~/.cache/git/credential/socket' '\ndiff --git a/t/t0303-credential-external.sh b/t/t0303-credential-external.sh\nindex f028fd14182..095574bfc6e 100755\n--- a/t/t0303-credential-external.sh\n+++ b/t/t0303-credential-external.sh\n@@ -45,6 +45,8 @@ test -z \"$GIT_TEST_CREDENTIAL_HELPER_SETUP\" ||\n helper_test_clean \"$GIT_TEST_CREDENTIAL_HELPER\"\n \n helper_test \"$GIT_TEST_CREDENTIAL_HELPER\"\n+helper_test_password_expiry_utc \"$GIT_TEST_CREDENTIAL_HELPER\"\n+helper_test_oauth_refresh_token \"$GIT_TEST_CREDENTIAL_HELPER\"\n \n if test -z \"$GIT_TEST_CREDENTIAL_HELPER_TIMEOUT\"; then\n \tsay \"# skipping timeout tests (GIT_TEST_CREDENTIAL_HELPER_TIMEOUT not set)\"\n\nbase-commit: 0df2c180904f6b709766f9c24669a9d01543f915\n-- \ngitgitgadget\n"},{"id":"478474","messageId":"pull.1469.v5.git.git.1686945306242.gitgitgadget@gmail.com","threadId":"59398","inReplyTo":"pull.1469.v4.git.git.1684306540947.gitgitgadget@gmail.com","subject":"[PATCH v5] credential/libsecret: store new attributes","fromName":"M Hickford via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2023-06-16T19:55:06Z","receivedAt":"2023-06-16T19:55:15Z","isPatch":true,"sender":{"key":"mirth.hickford@gmail.com","avatar":"https://avatars.githubusercontent.com/u/105314?v=4"},"body":"From: M Hickford <mirth.hickford@gmail.com>\n\nd208bfd (credential: new attribute password_expiry_utc, 2023-02-18)\nand a5c76569e7 (credential: new attribute oauth_refresh_token)\nintroduced new credential attributes.\n\nlibsecret assumes attribute values are non-confidential and\nunchanging, so we encode the new attributes in the secret, separated by\nnewline:\n\n    hunter2\n    password_expiry_utc=1684189401\n    oauth_refresh_token=xyzzy\n\nThis is extensible and backwards compatible. The credential protocol\nalready assumes that attribute values do not contain newlines.\n\nAlternatives considered: store password_expiry_utc in a libsecret\nattribute. This has the problem that libsecret creates new items\nrather than overwrites when attribute values change.\n\nSigned-off-by: M Hickford <mirth.hickford@gmail.com>\n---\n    credential/libsecret: store password_expiry_utc and oauth_refresh_token\n    \n    Test that storing credential with expiry overwrites any old credential\n    \n    Rename test users for clarity\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1469%2Fhickford%2Flibsecret-v5\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1469/hickford/libsecret-v5\nPull-Request: https://github.com/git/git/pull/1469\n\nRange-diff vs v4:\n\n 1:  07452a10372 ! 1:  38e02c5e24e credential/libsecret: store new attributes\n     @@ t/lib-credential.sh: helper_test_clean() {\n       \treject $1 https example.com store-user\n       \treject $1 https example.com user1\n       \treject $1 https example.com user2\n     -+\treject $1 https example.com user3\n     ++\treject $1 https example.com user-expiry\n     ++\treject $1 https example.com user-expiry-overwrite\n       \treject $1 https example.com user4\n       \treject $1 http path.tld user\n       \treject $1 https timeout.tld user\n     @@ t/lib-credential.sh: helper_test_timeout() {\n      +\t\tcheck approve $HELPER <<-\\EOF\n      +\t\tprotocol=https\n      +\t\thost=example.com\n     -+\t\tusername=user3\n     ++\t\tusername=user-expiry\n      +\t\tpassword=pass\n      +\t\tpassword_expiry_utc=9999999999\n      +\t\tEOF\n     @@ t/lib-credential.sh: helper_test_timeout() {\n      +\t\tcheck fill $HELPER <<-\\EOF\n      +\t\tprotocol=https\n      +\t\thost=example.com\n     -+\t\tusername=user3\n     ++\t\tusername=user-expiry\n      +\t\t--\n      +\t\tprotocol=https\n      +\t\thost=example.com\n     -+\t\tusername=user3\n     ++\t\tusername=user-expiry\n      +\t\tpassword=pass\n      +\t\tpassword_expiry_utc=9999999999\n      +\t\t--\n      +\t\tEOF\n      +\t'\n     ++\n     ++\ttest_expect_success \"helper ($HELPER) overwrites when password_expiry_utc changes\" '\n     ++\t\tcheck approve $HELPER <<-\\EOF &&\n     ++\t\tprotocol=https\n     ++\t\thost=example.com\n     ++\t\tusername=user-expiry-overwrite\n     ++\t\tpassword=pass1\n     ++\t\tpassword_expiry_utc=9999999998\n     ++\t\tEOF\n     ++\t\tcheck approve $HELPER <<-\\EOF &&\n     ++\t\tprotocol=https\n     ++\t\thost=example.com\n     ++\t\tusername=user-expiry-overwrite\n     ++\t\tpassword=pass2\n     ++\t\tpassword_expiry_utc=9999999999\n     ++\t\tEOF\n     ++\t\tcheck fill $HELPER <<-\\EOF &&\n     ++\t\tprotocol=https\n     ++\t\thost=example.com\n     ++\t\tusername=user-expiry-overwrite\n     ++\t\t--\n     ++\t\tprotocol=https\n     ++\t\thost=example.com\n     ++\t\tusername=user-expiry-overwrite\n     ++\t\tpassword=pass2\n     ++\t\tpassword_expiry_utc=9999999999\n     ++\t\tEOF\n     ++\t\tcheck reject $HELPER <<-\\EOF &&\n     ++\t\tprotocol=https\n     ++\t\thost=example.com\n     ++\t\tusername=user-expiry-overwrite\n     ++\t\tpassword=pass2\n     ++\t\tEOF\n     ++\t\tcheck fill $HELPER <<-\\EOF\n     ++\t\tprotocol=https\n     ++\t\thost=example.com\n     ++\t\tusername=user-expiry-overwrite\n     ++\t\t--\n     ++\t\tprotocol=https\n     ++\t\thost=example.com\n     ++\t\tusername=user-expiry-overwrite\n     ++\t\tpassword=askpass-password\n     ++\t\t--\n     ++\t\taskpass: Password for '\\''https://user-expiry-overwrite@example.com'\\'':\n     ++\t\tEOF\n     ++\t'\n      +}\n      +\n       helper_test_oauth_refresh_token() {\n\n\n .../libsecret/git-credential-libsecret.c      | 78 +++++++++++++++++--\n t/lib-credential.sh                           | 77 ++++++++++++++++++\n t/t0301-credential-cache.sh                   |  1 +\n t/t0303-credential-external.sh                |  2 +\n 4 files changed, 152 insertions(+), 6 deletions(-)\n\ndiff --git a/contrib/credential/libsecret/git-credential-libsecret.c b/contrib/credential/libsecret/git-credential-libsecret.c\nindex ef681f29d5b..31cf32ad969 100644\n--- a/contrib/credential/libsecret/git-credential-libsecret.c\n+++ b/contrib/credential/libsecret/git-credential-libsecret.c\n@@ -39,6 +39,8 @@ struct credential {\n \tchar *path;\n \tchar *username;\n \tchar *password;\n+\tchar *password_expiry_utc;\n+\tchar *oauth_refresh_token;\n };\n \n #define CREDENTIAL_INIT { 0 }\n@@ -54,6 +56,25 @@ struct credential_operation {\n \n /* ----------------- Secret Service functions ----------------- */\n \n+static const SecretSchema schema = {\n+\t\"org.git.Password\",\n+\t/* Ignore schema name during search for backwards compatibility */\n+\tSECRET_SCHEMA_DONT_MATCH_NAME,\n+\t{\n+\t\t/*\n+\t\t * libsecret assumes attribute values are non-confidential and\n+\t\t * unchanging, so we can't include oauth_refresh_token or\n+\t\t * password_expiry_utc.\n+\t\t */\n+\t\t{  \"user\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n+\t\t{  \"object\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n+\t\t{  \"protocol\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n+\t\t{  \"port\", SECRET_SCHEMA_ATTRIBUTE_INTEGER },\n+\t\t{  \"server\", SECRET_SCHEMA_ATTRIBUTE_STRING },\n+\t\t{  NULL, 0 },\n+\t}\n+};\n+\n static char *make_label(struct credential *c)\n {\n \tif (c->port)\n@@ -101,7 +122,7 @@ static int keyring_get(struct credential *c)\n \n \tattributes = make_attr_list(c);\n \titems = secret_service_search_sync(service,\n-\t\t\t\t\t   SECRET_SCHEMA_COMPAT_NETWORK,\n+\t\t\t\t\t   &schema,\n \t\t\t\t\t   attributes,\n \t\t\t\t\t   SECRET_SEARCH_LOAD_SECRETS | SECRET_SEARCH_UNLOCK,\n \t\t\t\t\t   NULL,\n@@ -117,6 +138,7 @@ static int keyring_get(struct credential *c)\n \t\tSecretItem *item;\n \t\tSecretValue *secret;\n \t\tconst char *s;\n+\t\tgchar **parts;\n \n \t\titem = items->data;\n \t\tsecret = secret_item_get_secret(item);\n@@ -130,8 +152,27 @@ static int keyring_get(struct credential *c)\n \n \t\ts = secret_value_get_text(secret);\n \t\tif (s) {\n-\t\t\tg_free(c->password);\n-\t\t\tc->password = g_strdup(s);\n+\t\t\t/*\n+\t\t\t * Passwords and other attributes encoded in following format:\n+\t\t\t *   hunter2\n+\t\t\t *   password_expiry_utc=1684189401\n+\t\t\t *   oauth_refresh_token=xyzzy\n+\t\t\t */\n+\t\t\tparts = g_strsplit(s, \"\\n\", 0);\n+\t\t\tif (g_strv_length(parts) >= 1) {\n+\t\t\t\tg_free(c->password);\n+\t\t\t\tc->password = g_strdup(parts[0]);\n+\t\t\t}\n+\t\t\tfor (int i = 1; i < g_strv_length(parts); i++) {\n+\t\t\t\tif (g_str_has_prefix(parts[i], \"password_expiry_utc=\")) {\n+\t\t\t\t\tg_free(c->password_expiry_utc);\n+\t\t\t\t\tc->password_expiry_utc = g_strdup(&parts[i][20]);\n+\t\t\t\t} else if (g_str_has_prefix(parts[i], \"oauth_refresh_token=\")) {\n+\t\t\t\t\tg_free(c->oauth_refresh_token);\n+\t\t\t\t\tc->oauth_refresh_token = g_strdup(&parts[i][20]);\n+\t\t\t\t}\n+\t\t\t}\n+\t\t\tg_strfreev(parts);\n \t\t}\n \n \t\tg_hash_table_unref(attributes);\n@@ -148,6 +189,7 @@ static int keyring_store(struct credential *c)\n \tchar *label = NULL;\n \tGHashTable *attributes = NULL;\n \tGError *error = NULL;\n+\tGString *secret = NULL;\n \n \t/*\n \t * Sanity check that what we are storing is actually sensible.\n@@ -162,13 +204,23 @@ static int keyring_store(struct credential *c)\n \n \tlabel = make_label(c);\n \tattributes = make_attr_list(c);\n-\tsecret_password_storev_sync(SECRET_SCHEMA_COMPAT_NETWORK,\n+\tsecret = g_string_new(c->password);\n+\tif (c->password_expiry_utc) {\n+\t\tg_string_append_printf(secret, \"\\npassword_expiry_utc=%s\",\n+\t\t\tc->password_expiry_utc);\n+\t}\n+\tif (c->oauth_refresh_token) {\n+\t\tg_string_append_printf(secret, \"\\noauth_refresh_token=%s\",\n+\t\t\tc->oauth_refresh_token);\n+\t}\n+\tsecret_password_storev_sync(&schema,\n \t\t\t\t    attributes,\n \t\t\t\t    NULL,\n \t\t\t\t    label,\n-\t\t\t\t    c->password,\n+\t\t\t\t    secret->str,\n \t\t\t\t    NULL,\n \t\t\t\t    &error);\n+\tg_string_free(secret, TRUE);\n \tg_free(label);\n \tg_hash_table_unref(attributes);\n \n@@ -198,7 +250,7 @@ static int keyring_erase(struct credential *c)\n \t\treturn EXIT_FAILURE;\n \n \tattributes = make_attr_list(c);\n-\tsecret_password_clearv_sync(SECRET_SCHEMA_COMPAT_NETWORK,\n+\tsecret_password_clearv_sync(&schema,\n \t\t\t\t    attributes,\n \t\t\t\t    NULL,\n \t\t\t\t    &error);\n@@ -238,6 +290,8 @@ static void credential_clear(struct credential *c)\n \tg_free(c->path);\n \tg_free(c->username);\n \tg_free(c->password);\n+\tg_free(c->password_expiry_utc);\n+\tg_free(c->oauth_refresh_token);\n \n \tcredential_init(c);\n }\n@@ -284,11 +338,19 @@ static int credential_read(struct credential *c)\n \t\t} else if (!strcmp(key, \"username\")) {\n \t\t\tg_free(c->username);\n \t\t\tc->username = g_strdup(value);\n+\t\t} else if (!strcmp(key, \"password_expiry_utc\")) {\n+\t\t\tg_free(c->password_expiry_utc);\n+\t\t\tc->password_expiry_utc = g_strdup(value);\n \t\t} else if (!strcmp(key, \"password\")) {\n \t\t\tg_free(c->password);\n \t\t\tc->password = g_strdup(value);\n \t\t\twhile (*value)\n \t\t\t\t*value++ = '\\0';\n+\t\t} else if (!strcmp(key, \"oauth_refresh_token\")) {\n+\t\t\tg_free(c->oauth_refresh_token);\n+\t\t\tc->oauth_refresh_token = g_strdup(value);\n+\t\t\twhile (*value)\n+\t\t\t\t*value++ = '\\0';\n \t\t}\n \t\t/*\n \t\t * Ignore other lines; we don't know what they mean, but\n@@ -314,6 +376,10 @@ static void credential_write(const struct credential *c)\n \t/* only write username/password, if set */\n \tcredential_write_item(stdout, \"username\", c->username);\n \tcredential_write_item(stdout, \"password\", c->password);\n+\tcredential_write_item(stdout, \"password_expiry_utc\",\n+\t\tc->password_expiry_utc);\n+\tcredential_write_item(stdout, \"oauth_refresh_token\",\n+\t\tc->oauth_refresh_token);\n }\n \n static void usage(const char *name)\ndiff --git a/t/lib-credential.sh b/t/lib-credential.sh\nindex f1ab92ba35c..72f52cfedb2 100644\n--- a/t/lib-credential.sh\n+++ b/t/lib-credential.sh\n@@ -43,6 +43,8 @@ helper_test_clean() {\n \treject $1 https example.com store-user\n \treject $1 https example.com user1\n \treject $1 https example.com user2\n+\treject $1 https example.com user-expiry\n+\treject $1 https example.com user-expiry-overwrite\n \treject $1 https example.com user4\n \treject $1 http path.tld user\n \treject $1 https timeout.tld user\n@@ -328,6 +330,81 @@ helper_test_timeout() {\n \t'\n }\n \n+helper_test_password_expiry_utc() {\n+\tHELPER=$1\n+\n+\ttest_expect_success \"helper ($HELPER) stores password_expiry_utc\" '\n+\t\tcheck approve $HELPER <<-\\EOF\n+\t\tprotocol=https\n+\t\thost=example.com\n+\t\tusername=user-expiry\n+\t\tpassword=pass\n+\t\tpassword_expiry_utc=9999999999\n+\t\tEOF\n+\t'\n+\n+\ttest_expect_success \"helper ($HELPER) gets password_expiry_utc\" '\n+\t\tcheck fill $HELPER <<-\\EOF\n+\t\tprotocol=https\n+\t\thost=example.com\n+\t\tusername=user-expiry\n+\t\t--\n+\t\tprotocol=https\n+\t\thost=example.com\n+\t\tusername=user-expiry\n+\t\tpassword=pass\n+\t\tpassword_expiry_utc=9999999999\n+\t\t--\n+\t\tEOF\n+\t'\n+\n+\ttest_expect_success \"helper ($HELPER) overwrites when password_expiry_utc changes\" '\n+\t\tcheck approve $HELPER <<-\\EOF &&\n+\t\tprotocol=https\n+\t\thost=example.com\n+\t\tusername=user-expiry-overwrite\n+\t\tpassword=pass1\n+\t\tpassword_expiry_utc=9999999998\n+\t\tEOF\n+\t\tcheck approve $HELPER <<-\\EOF &&\n+\t\tprotocol=https\n+\t\thost=example.com\n+\t\tusername=user-expiry-overwrite\n+\t\tpassword=pass2\n+\t\tpassword_expiry_utc=9999999999\n+\t\tEOF\n+\t\tcheck fill $HELPER <<-\\EOF &&\n+\t\tprotocol=https\n+\t\thost=example.com\n+\t\tusername=user-expiry-overwrite\n+\t\t--\n+\t\tprotocol=https\n+\t\thost=example.com\n+\t\tusername=user-expiry-overwrite\n+\t\tpassword=pass2\n+\t\tpassword_expiry_utc=9999999999\n+\t\tEOF\n+\t\tcheck reject $HELPER <<-\\EOF &&\n+\t\tprotocol=https\n+\t\thost=example.com\n+\t\tusername=user-expiry-overwrite\n+\t\tpassword=pass2\n+\t\tEOF\n+\t\tcheck fill $HELPER <<-\\EOF\n+\t\tprotocol=https\n+\t\thost=example.com\n+\t\tusername=user-expiry-overwrite\n+\t\t--\n+\t\tprotocol=https\n+\t\thost=example.com\n+\t\tusername=user-expiry-overwrite\n+\t\tpassword=askpass-password\n+\t\t--\n+\t\taskpass: Password for '\\''https://user-expiry-overwrite@example.com'\\'':\n+\t\tEOF\n+\t'\n+}\n+\n helper_test_oauth_refresh_token() {\n \tHELPER=$1\n \ndiff --git a/t/t0301-credential-cache.sh b/t/t0301-credential-cache.sh\nindex c02a3b5969c..8300faadea9 100755\n--- a/t/t0301-credential-cache.sh\n+++ b/t/t0301-credential-cache.sh\n@@ -29,6 +29,7 @@ test_atexit 'git credential-cache exit'\n \n # test that the daemon works with no special setup\n helper_test cache\n+helper_test_password_expiry_utc cache\n helper_test_oauth_refresh_token cache\n \n test_expect_success 'socket defaults to ~/.cache/git/credential/socket' '\ndiff --git a/t/t0303-credential-external.sh b/t/t0303-credential-external.sh\nindex f028fd14182..095574bfc6e 100755\n--- a/t/t0303-credential-external.sh\n+++ b/t/t0303-credential-external.sh\n@@ -45,6 +45,8 @@ test -z \"$GIT_TEST_CREDENTIAL_HELPER_SETUP\" ||\n helper_test_clean \"$GIT_TEST_CREDENTIAL_HELPER\"\n \n helper_test \"$GIT_TEST_CREDENTIAL_HELPER\"\n+helper_test_password_expiry_utc \"$GIT_TEST_CREDENTIAL_HELPER\"\n+helper_test_oauth_refresh_token \"$GIT_TEST_CREDENTIAL_HELPER\"\n \n if test -z \"$GIT_TEST_CREDENTIAL_HELPER_TIMEOUT\"; then\n \tsay \"# skipping timeout tests (GIT_TEST_CREDENTIAL_HELPER_TIMEOUT not set)\"\n\nbase-commit: 9e49351c3060e1fa6e0d2de64505b7becf157f28\n-- \ngitgitgadget\n"}]}