44import string
55import pathlib
66from django .contrib .auth import get_user_model
7+ from django .contrib .auth .hashers import check_password
78from django .core .management .base import BaseCommand
89from oauth2_provider .models import get_application_model
910
1415
1516
1617class Command (BaseCommand ):
17- """Management command for create Oauth2 application"""
18+ """Management command for create Oauth2 application.
19+
20+ Credential resolution order for ``client_id`` / ``client_secret``:
21+
22+ 1. Explicit value from the init-applications JSON file (highest priority,
23+ lets operators pin credentials via ``--set initApplications[...]``).
24+ 2. Mounted Kubernetes secret file at
25+ ``/var/run/secrets/drycc/passport/drycc-passport-<name>-<key|secret>``.
26+ This is the source of truth for credentials that the chart's
27+ ``passport-creds`` Secret already exposes to every consumer (controller,
28+ grafana, builder, manager, ...). Reading it here keeps the DB and the
29+ Secret consistent for *every* application, including m2m apps that
30+ have no public sub-domain (``prefix == ""``).
31+ 3. Newly generated random string (only when neither of the above is
32+ available, e.g. local dev without the volume mount).
33+
34+ Note: ``prefix`` only controls how ``redirect_uri`` is composed. It is
35+ intentionally NOT used to gate credential discovery -- m2m applications
36+ legitimately have an empty prefix.
37+ """
1838
1939 def add_arguments (self , parser ):
2040 super (Command , self ).add_arguments (parser )
@@ -28,32 +48,55 @@ def handle(self, *args, **options):
2848 user = User .objects .filter (is_superuser = True ).first ()
2949 for item in json .loads (pathlib .Path (base_path ).read_text ()):
3050 name = item ["name" ]
31- _ , updated = Application .objects .update_or_create (
32- name = name .lower (),
33- defaults = {
34- 'client_id' : self ._get_creds (item , "key" , 40 ),
35- 'client_secret' : self ._get_creds (item , "secret" , 60 ),
36- 'user' : user ,
37- 'redirect_uris' : self ._get_redirect_uri (item ),
38- 'authorization_grant_type' : item ['grant_type' ],
39- 'client_type' : 'public' ,
40- 'algorithm' : 'RS256'
41- }
51+ client_id = self ._get_creds (item , "key" , 40 )
52+ client_secret = self ._get_creds (item , "secret" , 60 )
53+ defaults = {
54+ 'client_id' : client_id ,
55+ 'user' : user ,
56+ 'redirect_uris' : self ._get_redirect_uri (item ),
57+ 'authorization_grant_type' : item .get ('grant_type' , 'public' ),
58+ 'client_type' : item .get ('client_type' , 'public' ),
59+ 'allowed_scopes' : item .get ('allowed_scopes' , '' ),
60+ 'algorithm' : 'RS256' ,
61+ }
62+ existing = Application .objects .filter (name = name .lower ()).first ()
63+ secret_unchanged = (
64+ existing is not None
65+ and check_password (client_secret , existing .client_secret )
66+ )
67+ if not secret_unchanged :
68+ defaults ['client_secret' ] = client_secret
69+ _ , created = Application .objects .update_or_create (
70+ name = name .lower (), defaults = defaults ,
4271 )
43- if updated :
44- self .stdout .write ('Drycc % app created' % name )
72+ if created :
73+ self .stdout .write ('Drycc %s app created' % name )
4574 else :
46- self .stdout .write ('Drycc % app updated' % name )
75+ self .stdout .write ('Drycc %s app updated' % name )
4776
4877 def _get_creds (self , item , suffix , size ):
49- name , secret , prefix = item ["name" ], item [suffix ], item ["prefix" ]
50- if not secret :
51- default_secret_path = os .path .join (
52- secrets_path , "drycc-passport-%s-%s" % (name , suffix ))
53- if prefix and os .path .exists (default_secret_path ):
54- secret = pathlib .Path (default_secret_path ).read_text ()
55- else :
56- secret = '' .join ([random .choice (string .ascii_letters ) for _ in range (size )])
78+ name = item ["name" ]
79+ secret = item .get (suffix )
80+ if secret :
81+ self .stdout .write (
82+ '[%s/%s] credential source: init-config' % (name , suffix ))
83+ return secret
84+
85+ default_secret_path = os .path .join (
86+ secrets_path , "drycc-passport-%s-%s" % (name , suffix ))
87+ if os .path .exists (default_secret_path ):
88+ # ``.strip()`` defends against trailing newlines that some
89+ # tooling adds when materialising secrets onto disk.
90+ secret = pathlib .Path (default_secret_path ).read_text ().strip ()
91+ self .stdout .write (
92+ '[%s/%s] credential source: mounted-file (%s)'
93+ % (name , suffix , default_secret_path ))
94+ return secret
95+
96+ secret = '' .join (random .choice (string .ascii_letters ) for _ in range (size ))
97+ self .stdout .write (
98+ '[%s/%s] credential source: generated-random '
99+ '(no init value, no mounted file)' % (name , suffix ))
57100 return secret
58101
59102 def _get_redirect_uri (self , item ):
0 commit comments