Establish scoped KeyCape factor custody and verified automatic renewal
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a092fe-13b1-7f12-ac74-7d258af4d79c
This commit is contained in:
parent
b75729b799
commit
2e2c31d237
22 changed files with 1169 additions and 1 deletions
72
scripts/keycape_factor_provider.py
Normal file
72
scripts/keycape_factor_provider.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
"""Run only inside the provider; secret-bearing stdin/stdout stay in owner pipes."""
|
||||
import contextlib,io,json,logging,sys,time,base64,urllib.request,urllib.error,urllib.parse
|
||||
USER="keycape-factor-reader"
|
||||
BASELINE="keycape-preserve-existing-admins"
|
||||
READER="keycape-factor-reader-coulomb"
|
||||
REALM="coulomb"
|
||||
|
||||
class NoRedirect(urllib.request.HTTPRedirectHandler):
|
||||
def redirect_request(self,*args,**kwargs):return None
|
||||
|
||||
def request(path,payload=None,token=None):
|
||||
headers={}
|
||||
if token:headers["Authorization"]=token
|
||||
data=urllib.parse.urlencode(payload).encode() if payload is not None else None
|
||||
req=urllib.request.Request("http://127.0.0.1:8080"+path,data=data,headers=headers)
|
||||
try:
|
||||
with urllib.request.build_opener(NoRedirect()).open(req,timeout=15) as r:
|
||||
raw=r.read(1048577)
|
||||
if len(raw)>1048576:raise RuntimeError("provider response too large")
|
||||
return r.status,json.loads(raw)
|
||||
except urllib.error.HTTPError as e:return e.code,{}
|
||||
|
||||
def token_result(password):
|
||||
code,auth=request("/auth",{"username":USER,"password":password})
|
||||
value=auth.get("result",{}).get("value",{})
|
||||
if code!=200 or not auth.get("result",{}).get("status") or value.get("role")!="admin" or value.get("username")!=USER:raise RuntimeError("service authentication failed")
|
||||
token=value.get("token","")
|
||||
claims=json.loads(base64.urlsafe_b64decode(token.split('.')[1]+'==='))
|
||||
expiry=int(claims['exp'])
|
||||
if not 300<expiry-time.time()<=7200:raise RuntimeError("unsupported service token lifetime")
|
||||
code,listing=request("/token/?tokenrealm=coulomb&active=True&page=1&pagesize=1",token=token)
|
||||
lv=listing.get("result",{}).get("value",{})
|
||||
if code!=200 or not listing.get("result",{}).get("status") or not isinstance(lv.get("tokens"),list) or not isinstance(lv.get("count"),int):raise RuntimeError("factor lookup failed "+str(code)+" "+str(bool(listing.get("result",{}).get("status")))+" "+str(isinstance(lv.get("tokens"),list))+" "+str(isinstance(lv.get("count"),int)))
|
||||
code,_=request("/policy/",token=token)
|
||||
if code not in (401,403):raise RuntimeError("service has unexpected administration rights")
|
||||
return {"token":token,"expires_at":expiry,"cross_user_factor_visible":lv["count"]>0,"policy_read_denied":True}
|
||||
|
||||
def bootstrap(password):
|
||||
logging.disable(logging.CRITICAL)
|
||||
from privacyidea.app import create_app
|
||||
from privacyidea.lib.policy import PolicyClass,set_policy
|
||||
from privacyidea.lib.auth import get_db_admins,create_db_admin
|
||||
app=create_app(config_name="production")
|
||||
with app.app_context():
|
||||
policies=PolicyClass().list_policies(scope="admin",active=True)
|
||||
# Source preflight found no policies. Retry accepts only our exact two policies.
|
||||
if any(p['name'] not in [BASELINE,READER] for p in policies):raise RuntimeError("admin policy baseline changed")
|
||||
for p in policies:
|
||||
expected=({"adminuser":["*","!"+USER],"realm":[],"action":{"*":True}} if p['name']==BASELINE else {"adminuser":[USER],"realm":[REALM],"action":{"tokenlist":True}})
|
||||
if any(p.get(k)!=v for k,v in expected.items()):raise RuntimeError("existing managed policy differs")
|
||||
existing={a.username for a in get_db_admins()}
|
||||
if USER in existing and not policies:raise RuntimeError("service name already belongs to another setup")
|
||||
if BASELINE not in {p['name'] for p in policies}:
|
||||
set_policy(name=BASELINE,scope="admin",action="*",adminuser=["*","!"+USER],description="Preserve default rights for existing administrative identities; exclude factor reader")
|
||||
if READER not in {p['name'] for p in policies}:
|
||||
set_policy(name=READER,scope="admin",action="tokenlist",adminuser=USER,realm=REALM,description="KeyCape factor listing in coulomb only")
|
||||
if USER not in existing:create_db_admin(USER,password=password)
|
||||
return token_result(password)
|
||||
|
||||
def main():
|
||||
data=json.load(sys.stdin)
|
||||
if data.get('username')!=USER or not isinstance(data.get('password'),str) or len(data['password'])<32:return 2
|
||||
with contextlib.redirect_stdout(io.StringIO()),contextlib.redirect_stderr(io.StringIO()):
|
||||
result=bootstrap(data['password']) if data.get('operation')=='bootstrap' else token_result(data['password'])
|
||||
sys.stdout.write(json.dumps(result));return 0
|
||||
if __name__=='__main__':
|
||||
try:code=main()
|
||||
except RuntimeError as exc:
|
||||
sys.stdout.write(json.dumps({"failure":str(exc)}));code=1
|
||||
except Exception as exc:
|
||||
sys.stdout.write(json.dumps({"failure_type":type(exc).__name__}));code=1
|
||||
raise SystemExit(code)
|
||||
Loading…
Add table
Add a link
Reference in a new issue