4343 "type" : "personal_access_token" ,
4444 "account_id" : 17 ,
4545 "name" : "key" ,
46- "scope" : ["scan" ],
46+ "scope" : [
47+ "scan" ,
48+ "honeytokens:write" ,
49+ "honeytokens:check" ,
50+ "nhi:send-inventory" ,
51+ ],
4752 "expire_at" : None ,
4853 }
4954)
5964 {"detail" : "Invalid GitGuardian API key." }, 401
6065)
6166
67+ API_TOKENS_ENDPOINT = "/v1/api_tokens/self"
68+
69+ VALID_API_TOKENS_RESPONSE = create_json_response (
70+ {
71+ "id" : "00000000-0000-0000-0000-000000000001" ,
72+ "name" : "key" ,
73+ "workspace_id" : 17 ,
74+ "type" : "personal_access_token" ,
75+ "status" : "active" ,
76+ "created_at" : "2021-01-01T00:00:00+00:00" ,
77+ "scopes" : [
78+ "scan" ,
79+ "honeytokens:write" ,
80+ "honeytokens:check" ,
81+ "nhi:send-inventory" ,
82+ ],
83+ }
84+ )
85+
6286METADATA_ENDPOINT = "/v1/metadata"
6387
6488VALID_METADATA_RESPONSE = create_json_response (
@@ -92,6 +116,7 @@ def test_auth_login_token(self, monkeypatch, cli_fs_runner, test_case):
92116
93117 if test_case == "valid" :
94118 self ._request_mock .add_GET (TOKEN_ENDPOINT , VALID_TOKEN_RESPONSE )
119+ self ._request_mock .add_GET ("/v1/api_tokens/self" , VALID_API_TOKENS_RESPONSE )
95120 elif test_case == "invalid_scope" :
96121 self ._request_mock .add_GET (
97122 TOKEN_ENDPOINT , VALID_TOKEN_INVALID_SCOPE_RESPONSE
@@ -119,6 +144,53 @@ def test_auth_login_token(self, monkeypatch, cli_fs_runner, test_case):
119144
120145 self ._request_mock .assert_all_requests_happened ()
121146
147+ def test_auth_login_token_missing_default_scopes (self , monkeypatch , cli_fs_runner ):
148+ """
149+ GIVEN an API token that has scan but is missing other default scopes
150+ WHEN the auth login command is called with --method=token
151+ THEN login succeed with a message listing the missing permissions
152+ AND the token is saved (scan still works)
153+ AND a warning is displayed
154+ """
155+ token = "mysupertoken"
156+ instance = "https://dashboard.gitguardian.com"
157+ cmd = ["auth" , "login" , "--method=token" , f"--instance={ instance } " ]
158+
159+ self ._request_mock .add_GET (
160+ TOKEN_ENDPOINT ,
161+ create_json_response (
162+ {
163+ "type" : "personal_access_token" ,
164+ "account_id" : 17 ,
165+ "name" : "key" ,
166+ "scope" : ["scan" ],
167+ "expire_at" : None ,
168+ }
169+ ),
170+ )
171+ self ._request_mock .add_GET (
172+ "/v1/api_tokens/self" ,
173+ create_json_response (
174+ {
175+ ** VALID_API_TOKENS_RESPONSE .json (),
176+ "scopes" : ["scan" ],
177+ }
178+ ),
179+ )
180+
181+ result = cli_fs_runner .invoke (cli , cmd , color = False , input = token + "\n " )
182+
183+ assert result .exit_code == 0
184+ assert "Warning: the following scopes were not granted:" in result .output
185+ assert "honeytokens:write" in result .output
186+ assert "nhi:send-inventory" in result .output
187+ assert "Some features may require additional permissions" in result .output
188+
189+ config = Config ()
190+ assert config .auth_config .get_instance (instance ).account .token == token
191+
192+ self ._request_mock .assert_all_requests_happened ()
193+
122194 def test_auth_login_token_default_instance (self , monkeypatch , cli_fs_runner ):
123195 """
124196 GIVEN a valid API token
@@ -130,6 +202,7 @@ def test_auth_login_token_default_instance(self, monkeypatch, cli_fs_runner):
130202 assert len (config .auth_config .instances ) == 0
131203
132204 self ._request_mock .add_GET (TOKEN_ENDPOINT , VALID_TOKEN_RESPONSE )
205+ self ._request_mock .add_GET ("/v1/api_tokens/self" , VALID_API_TOKENS_RESPONSE )
133206
134207 cmd = ["auth" , "login" , "--method=token" ]
135208
@@ -179,6 +252,7 @@ def test_api_instance_url(
179252 token = "mysupertoken"
180253 cmd = ["auth" , "login" , "--method=token" , f"--instance={ cmd_line_instance } " ]
181254 self ._request_mock .add_GET (TOKEN_ENDPOINT , VALID_TOKEN_RESPONSE )
255+ self ._request_mock .add_GET ("/v1/api_tokens/self" , VALID_API_TOKENS_RESPONSE )
182256 result = cli_fs_runner .invoke (cli , cmd , color = False , input = token + "\n " )
183257 config = Config ()
184258 config_instance_urls = [
@@ -208,6 +282,7 @@ def test_auth_login_token_suggests_set_instance(
208282 assert not Config ().auth_config .instances
209283
210284 self ._request_mock .add_GET (TOKEN_ENDPOINT , VALID_TOKEN_RESPONSE )
285+ self ._request_mock .add_GET ("/v1/api_tokens/self" , VALID_API_TOKENS_RESPONSE )
211286
212287 cmd = ["auth" , "login" , "--method=token" ]
213288 if instance :
@@ -229,8 +304,11 @@ def test_auth_login_token_update_existing_config(self, monkeypatch, cli_fs_runne
229304 THEN the instance configuration is created if it doesn't exist, or updated otherwise
230305 """
231306 self ._request_mock .add_GET (TOKEN_ENDPOINT , VALID_TOKEN_RESPONSE )
307+ self ._request_mock .add_GET ("/v1/api_tokens/self" , VALID_API_TOKENS_RESPONSE )
232308 self ._request_mock .add_GET (TOKEN_ENDPOINT , VALID_TOKEN_RESPONSE )
309+ self ._request_mock .add_GET ("/v1/api_tokens/self" , VALID_API_TOKENS_RESPONSE )
233310 self ._request_mock .add_GET (TOKEN_ENDPOINT , VALID_TOKEN_RESPONSE )
311+ self ._request_mock .add_GET ("/v1/api_tokens/self" , VALID_API_TOKENS_RESPONSE )
234312
235313 instance = "https://dashboard.gitguardian.com"
236314 cmd = ["auth" , "login" , "--method=token" , f"--instance={ instance } " ]
@@ -277,6 +355,7 @@ def test_auth_login_token_from_stdin(self, monkeypatch, cli_fs_runner):
277355 assert len (config .auth_config .instances ) == 0
278356
279357 self ._request_mock .add_GET (TOKEN_ENDPOINT , VALID_TOKEN_RESPONSE )
358+ self ._request_mock .add_GET ("/v1/api_tokens/self" , VALID_API_TOKENS_RESPONSE )
280359
281360 token = "mysupertoken"
282361
@@ -619,7 +698,37 @@ def test_scopes(self, cli_fs_runner, monkeypatch):
619698 assert exit_code == ExitCode .SUCCESS , output
620699
621700 self ._webbrowser_open_mock .assert_called_once ()
622- self ._assert_open_url (scope_set = {"scan" , "honeytokens:write" , "teams:read" })
701+ self ._assert_open_url (
702+ scope_set = {
703+ "scan" ,
704+ "honeytokens:write" ,
705+ "honeytokens:check" ,
706+ "nhi:send-inventory" ,
707+ "teams:read" ,
708+ }
709+ )
710+
711+ def test_missing_default_scopes_fails_with_warning (
712+ self , cli_fs_runner , monkeypatch
713+ ):
714+ """
715+ GIVEN the backend grants only a subset of the default scopes
716+ WHEN the web login flow completes
717+ THEN the command succeed and lists the missing permissions
718+ AND the token is still saved
719+ """
720+ self .prepare_mocks (
721+ monkeypatch , missing_scopes = ["honeytokens:write" , "nhi:send-inventory" ]
722+ )
723+ exit_code , output = self .run_cmd (cli_fs_runner )
724+
725+ assert exit_code == ExitCode .SUCCESS
726+ assert "Warning: the following scopes were not granted:" in output
727+ assert "honeytokens:write" in output
728+ assert "nhi:send-inventory" in output
729+ assert "Some features may require additional permissions" in output
730+
731+ self ._assert_config ("mysupertoken" )
623732
624733 def prepare_mocks (
625734 self ,
@@ -632,6 +741,7 @@ def prepare_mocks(
632741 sso_url = None ,
633742 downsized_token : Optional [bool ] = False ,
634743 scopes : Optional [str ] = None ,
744+ missing_scopes : Optional [list ] = None ,
635745 ):
636746 """
637747 Configure self._request_mock to emulate HTTP requests
@@ -707,6 +817,12 @@ def prepare_mocks(
707817 if lifetime is not None :
708818 expire_at = self ._get_expiry_date ().isoformat ()
709819 token_response_payload ["expire_at" ] = expire_at
820+ if missing_scopes :
821+ token_response_payload ["scope" ] = [
822+ s
823+ for s in token_response_payload ["scope" ]
824+ if s not in missing_scopes
825+ ]
710826
711827 # mock api call to exchange the code against a valid access token
712828 response = create_json_response ({"key" : token , ** token_response_payload })
@@ -729,6 +845,19 @@ def prepare_mocks(
729845 ),
730846 )
731847
848+ if login_result == LoginResult .SUCCESS :
849+ scopes = [
850+ s
851+ for s in VALID_API_TOKENS_RESPONSE .json ()["scopes" ]
852+ if not missing_scopes or s not in missing_scopes
853+ ]
854+ self ._request_mock .add_GET (
855+ "/v1/api_tokens/self" ,
856+ create_json_response (
857+ {** VALID_API_TOKENS_RESPONSE .json (), "scopes" : scopes }
858+ ),
859+ )
860+
732861 def run_cmd (self , cli_fs_runner , method = "web" ):
733862 """
734863 Run the auth login method within a virtual cli.
@@ -814,7 +943,12 @@ def _assert_open_url(
814943 also check if the port of the redirect url is the one expected depending on occupied ports
815944 """
816945 if scope_set is None :
817- scope_set = {"scan" }
946+ scope_set = {
947+ "scan" ,
948+ "honeytokens:write" ,
949+ "honeytokens:check" ,
950+ "nhi:send-inventory" ,
951+ }
818952
819953 (url ,), kwargs = self ._webbrowser_open_mock .call_args_list [0 ]
820954 parsed_url = urlparse .urlparse (url )
@@ -1059,6 +1193,7 @@ def _add_token_endpoints(self, post_checker=None):
10591193 post_checker ,
10601194 )
10611195 self ._request_mock .add_GET (TOKEN_ENDPOINT , create_json_response (token_payload ))
1196+ self ._request_mock .add_GET ("/v1/api_tokens/self" , VALID_API_TOKENS_RESPONSE )
10621197 return token
10631198
10641199 def test_oob_happy_path (self , cli_fs_runner , monkeypatch ):
0 commit comments