GNL
Dokümanlar menüsü
Core · Ücretsiz@gnldev/auth

Açık-çekirdek auth (roleAuth)

Ücretsiz, opt-in kimlik katmanı: bearer ya da basic token üstünde dört sınıf (superAdmin / admin / client / viewer). Ne kadar iş yapacağına göre değil, TOKEN'I KİMİN TAŞIDIĞINA göre seçilir.

Ne işe yarar / ne zaman kullanılır#

GNL'in REST API'si ve Studio'su varsayılan olarak açıktır — auth yapılandırılmadıysa herkes okuyup yazabilir. roleAuth bunu tek satırda kapatır. Dört sınıfın sebebi şu: tek bir “admin” birbiriyle ilgisiz iki işi birden yapıyordu — sadece ajan koşturması gereken bir uygulama, koşuları iptal eden, bütçe düzenleyen ve org'un tüm geçmişini okuyan token'ı taşımak zorundaydı.

Bu katman açık çekirdek ve çizgi bilinçli: izolasyon ücretsiz — organizasyon sınırı, dört sınıf ve bir son kullanıcının sohbetlerini diğerinden ayıran resourceId kuralları burada, hiçbiri ödeme yapınca açılmıyor. Paralı olan kimlik yönetimi: SSO, kullanıcı başına hesap, denetim kaydı ve dört sabit sınıf yerine kişi başına izin. @gnldev/auth-ee aynı AuthProvider sözleşmesini uygular, yani yukarı geçmek çağrı yerlerinizi değiştirmez.

Kurulum / import#

paket
import { roleAuth, type Cred } from '@gnldev/auth';
import { createRestApi } from '@gnldev/server';
import { createStudioApp } from '@gnldev/studio';

@gnldev/auth tek başına bağımsız bir pakettir; @gnldev/server (REST API) ve @gnldev/studio her ikisi de auth seçeneğini AuthProvider | undefined olarak kabul eder — aynı provider hem REST hem Studio'ya aynı anda verilir.

Adım adım kullanım#

1. Her sınıf için ortamdan bir Cred üretin. Sınıflar aynı şeyin kademeleri değil — client BACKEND'inizin taşıdığı, admin bir KİŞİNİN taşıdığı şeydir:

src/index.ts
const cred = (name: string): Cred | undefined =>
  process.env.GNL_NO_AUTH === '1'
    ? undefined
    : { token: process.env[`GNL_${name}_TOKEN`], orgId: 'acme' };

// The credential your APPLICATION carries runs agents, manages nothing.
const clientAuth = roleAuth({ client: cred('CLIENT') });

// The credential a PERSON carries, for Studio.
const operatorAuth = roleAuth({ admin: cred('ADMIN'), viewer: cred('VIEWER') });

2. Her host'a kendisine ait kimliği verin. Eskiden ikisine de tek bir auth nesnesi geçiriliyordu; artık geçirilmemeli, çünkü Studio bir operatör konsoludur ve uygulama kimliğini son kullanıcı bazında hizmete almak yerine tümden reddeder (403):

src/index.ts
// Your backend calls this one.
app.route('/api', createRestApi(config, { title: 'My API', auth: clientAuth }));

// A person opens this one. Studio refuses an application credential outright.
app.route('/studio', createStudioApp({
  reader: toJournal(storage.runs),
  apiBase: '/studio',
  gnl: createStudioRunner(gnl, config, { toJsonSchema: aiToolSchema }),
  auth: operatorAuth,
}));

3. Backend'iniz REST API'yi client token'ıyla çağırır ve her isteğin kimin adına yapıldığını söyler. Bir client kimliği tek token'la birçok kullanıcıya hizmet ettiği için resourceId zorunludur — verilmezse istek kapsamsız hizmet almak yerine reddedilir:

# resourceId names the end user this request acts for. A client credential
# serves many of them, so it is required — without it the call is a 400.
curl -s -X POST http://localhost:3001/api/agents/support/run \
  -H 'content-type: application/json' \
  -H "authorization: Bearer $GNL_CLIENT_TOKEN" \
  -d '{"runId":"r-882","prompt":"where is my order?","threadId":"t-ayse-1","resourceId":"u-ayse"}'

# Ask for one end user's data, and have ownership checked:
curl -s "http://localhost:3001/api/runs?resourceId=u-ayse" -H "authorization: Bearer $GNL_CLIENT_TOKEN"

Studio'nun SSE bağlantıları Authorization başlığı gönderemez, o yüzden aynı bearer token ?token= ile de kabul edilir (yalnız bearer, yalnız GET). Bu değer proxy loglarına ve tarayıcı geçmişine düşebilir — mümkün olduğunda Studio'nun kısa ömürlü ?ticket= akışını tercih edin.

Kapsam, rol ve aralarındaki tavan#

Buradaki yetkilendirme, bilerek ayrı tutulan iki eksendir. Kapsam, bir kimliğin nerede iş görebileceğidir — tek bir organizasyon ya da bütün platform. Rol ise ne yapabileceğidir — okuma, koşturma, yönetme. İkisini birbirine karıştırmak, yalnız tek bir kiracıyı yönetmesi gereken bir kimliğin hepsini yönetmesiyle biten yoldur.

Platform kapsamı açık bir granttır: ayrılmış platform-admin rolü. Bir kimliğin orgId taşımıyor olmasından asla türetilmez. O çıkarım klasik fail-open ayak kapanıdır — organizasyon vermeyi unutmak bir süper-admin doğururdu — bu yüzden katı model, grant'ı olmayan bağsız kimliği { kind: 'none' } sayar ve reddeder.

principalScope — çözülmüş "nerede"
import { principalScope, isPlatformAdmin } from '@gnldev/auth';

principalScope({ id: 'u1', roles: ['platform-admin'] });  // { kind: 'platform' }
principalScope({ id: 'u2', roles: ['admin'], orgId: 'acme' }); // { kind: 'org', orgId: 'acme' }
principalScope({ id: 'u3', roles: ['admin'] });           // { kind: 'none' } -> strict model denies

isPlatformAdmin({ id: 'u3', roles: ['admin'] });          // false — no orgId is NOT a platform grant

Öncelik bilinçlidir: açık platform grant'ı org bağını yener, çünkü platform-admin zaten organizasyonlar-arası olmak içindir. orgId taşıyan geri kalan her şey org kapsamlıdır, onun da dışında kalan hiçbir şeydir.

Tavan. Yalnız hedefin organizasyonunu doğrulayan bir kullanıcı-yönetimi yüzeyi bir delik bırakır: organizasyona bağlı bir yönetici kendisine platform-admin rolünü verip kendi organizasyonunun dışına çıkabilir. assertAssignablePrivileges bunu kapatır — hiç kimse kendisinde olmayan bir ayrıcalığı veremez.

assertAssignablePrivileges — kimse kendi tavanının üstünü veremez
import { assertAssignablePrivileges } from '@gnldev/auth';

const acmeAdmin = { id: 'u2', roles: ['admin'], orgId: 'acme' };

assertAssignablePrivileges(acmeAdmin, { roles: ['member'] });
// { ok: true } — an ordinary org role, the target keeps orgId 'acme'

assertAssignablePrivileges(acmeAdmin, { roles: ['platform-admin'] });
// { ok: false, reason: "only a platform-admin can grant the 'platform-admin' role" }

assertAssignablePrivileges(acmeAdmin, { permissions: ['*'] });
// { ok: false, reason: "only a platform-admin can grant the '*' (all-permissions) grant" }

Kural bilerek asgaridir. Bir platform-admin her şeyi atayabilir. Başka herkes ne platform-admin rolünü ne de '*' tüm-izinler grant'ını verebilir — ikincisi aynı yükseltmenin rol ekseni yerine izin ekseninde ifade edilmiş hâlidir. Sıradan organizasyon rolleri atanabilir kalır, çünkü hedef kendi orgId'sini korur ve hiçbir şey organizasyon sınırını geçmez.

API referansı#

fnroleAuth

{ superAdmin?, admin?, client?, viewer? } → AuthProvider | undefined. Hiçbir sınıf verilmezse undefined döner (opt-in). Yalnızca { admin, viewer } verildiğinde, iki yeni sınıf hiç yokmuş gibi davranır.

fnprincipalScope

Kapsamı türetir: { kind: 'platform' } | { kind: 'org', orgId } | { kind: 'none' }. Açık platform grant'ı org bağını yener.

fnisPlatformAdmin

Yalnız principal açık platform-admin rolünü taşıyorsa true. Bağsız olmak (orgId yokluğu) bilerek yeterli değildir.

fnassertAssignablePrivileges

Kullanıcı oluşturma/güncellemede ayrıcalık tavanı: platform-admin olmayan biri platform-admin rolünü ya da '*' iznini vermeye çalışırsa { ok: false, reason } döner.

typePrincipalScope

Bir kimliğin çözülmüş 'nerede'si — rolünden bağımsız.

typeAssignabilityResult

{ ok: true } | { ok: false, reason } — tavan kontrolünün sonucu.

typeCLIENT_WRITES

Bir client kimliğinin yapabileceği yazma işlemlerinin tam kümesi (agents:run, workflow:run, run:cancel). BEYAZ LİSTEDİR — bu kümede adı geçmeyen bir yazma reddedilir, sonraki sürümlerde eklenen rotalar dahil.

typePLATFORM_ADMIN_ROLE

superAdmin'in taşıdığı, organizasyonlar-arası ayrılmış yetki. Eksik bir orgId'den ASLA türetilmez — o çıkarım, unutulan bir yapılandırma satırını organizasyonlar-arası bir süper admine çevirirdi.

fnmakeGate

AuthProvider'ı ortak bir Hono kapısına (allow/deny) çevirir — REST ve Studio bu mantığı tekrar yazmaz.

fnprincipalOf

allow() sırasında doğrulanan Principal'ı context'ten okur (aynı istek içinde) — org/audit buradan türetilir.

fnnormalizeAuth

AuthProvider | ReadWriteAuth | undefined → AuthProvider | undefined. Host'lar tek tipe indirger.

fnfromReadWrite

Eski { read, write } predicate çiftini (StudioAuth) AuthProvider'a sarar — geri uyumluluk köprüsü.

typeAuthProvider

Stabil sözleşme: authenticate(c), authorize(principal, c, ctx), opsiyonel capabilities().

typePrincipal

Kimliği doğrulanmış özne: { id?, roles, orgId?, permissions? }. Ücretsiz katman roles kullanır; 'permissions' paralı RBAC'in doldurduğu alandır.

typeDecision

{ allow: true } | { allow: false, status?: 401 | 403, reason? } — yetki kararı.

typeCred

{ token?, user?, pass?, orgId?, platformAdmin? } — bir sınıfın bearer token'ı ve/veya basic kimliği. 'orgId' kimliği tek bir organizasyona bağlar; başka bir organizasyonu adlandıran istek reddedilir. 'platformAdmin: true' herhangi bir sınıfa ayrılmış platform-admin rolünü enjekte eder — 'superAdmin' sınıfının adıyla taşıdığı organizasyonlar-arası yetkinin statik-yapılandırma kapısıdır. Sınıfı tercih edin: o, kimliğin NE OLDUĞUNU söyler; diğeri sıradan bir admin gibi okunan birine yetki ekler.

typeReadWriteAuth

{ read?, write? } predicate çifti — fromReadWrite ile AuthProvider'a dönüştürülür.

Not
Kapı opt-in: kimlik verilmeden roleAuth({}) undefined döner ve GNL açık davranışını korur — geriye dönük kırılma yok. Ama üretimde sağlayıcının olmaması sessiz bir açık kapı değil, hatadır.
İpucu
Aynı AuthProvider arayüzünü SSO/RBAC/çok-organizasyonluluk gerektiren dağıtımlar için paralı @gnldev/auth-ee implemente eder — geçiş REST/Studio tarafında kod değişikliği gerektirmez, yalnızca auth değerini değiştirmek yeterlidir.