Skip to content

feat: decouple ziti enrollment from RegisterApp, add EnrollApp RPC #4

Description

@rowan-stein

Request

Refactor RegisterApp to stop creating OpenZiti resources at registration time. Add a new EnrollApp RPC that apps call at startup to self-enroll: exchange their service_token for a fresh OpenZiti identity.

Specification

1. RegisterApp Refactor (internal/server/server.go)

Remove all ziti identity creation from RegisterApp:

  • Remove the CreateAppIdentity call and zitiResp variable
  • Remove cleanupZitiIdentity calls on error paths
  • Set ZitiIdentityID: "" and ZitiServiceID: "" in CreateAppInput

After refactoring, RegisterApp should:

  1. Validate slug/name
  2. Create platform identity (identity service)
  3. Generate + hash service token
  4. Write FGA tuple
  5. Create DB record with empty ziti_identity_id and ziti_service_id
  6. Return App + service_token

The cleanupZitiIdentity helper method is NOT removed — it's still used by DeleteApp and EnrollApp.

2. New EnrollApp Method (internal/server/server.go)

func (s *Server) EnrollApp(ctx context.Context, req *appsv1.EnrollAppRequest) (*appsv1.EnrollAppResponse, error) {
    // 1. Validate: service_token must be non-empty → InvalidArgument
    // 2. SHA-256 hash the raw token (same scheme as newServiceToken)
    // 3. GetAppByServiceTokenHash → NotFound if no match
    // 4. If app already has ziti resources: cleanupZitiIdentity (idempotent re-enrollment)
    // 5. CreateAppIdentity via ziti-management → Internal on failure
    // 6. UpdateAppZitiIdentity in DB → Internal on failure (cleanup ziti on DB error)
    // 7. Return EnrollAppResponse{identity_json, identity_id}
}

Error codes:

Failure gRPC Code
Empty service_token InvalidArgument
App not found NotFound
CreateAppIdentity fails Internal
DB update fails Internal

Authentication: EnrollApp does NOT use identityFromMetadata. The app authenticates via the service_token in the request body (it has no platform identity header yet).

Idempotency: Safe to call on every pod restart. Old ziti identity/service are cleaned up, new ones created.

3. Store Changes (internal/store/store.go)

New method:

func (s *Store) UpdateAppZitiIdentity(ctx context.Context, id uuid.UUID, zitiIdentityID string, zitiServiceID string) error {
    result, err := s.pool.Exec(ctx,
        `UPDATE apps SET ziti_identity_id = $1, ziti_service_id = $2, updated_at = NOW() WHERE id = $3`,
        zitiIdentityID,
        zitiServiceID,
        id,
    )
    if err != nil {
        return err
    }
    if result.RowsAffected() == 0 {
        return NotFound("app")
    }
    return nil
}

4. No Migration Needed

ziti_identity_id and ziti_service_id are already TEXT NOT NULL DEFAULT ''. Empty strings work as the "not yet enrolled" sentinel.

5. Gateway Impact

  • Gateway appproxy.go already handles empty ziti_service_id correctly (returns 502 "app service id missing")
  • No changes needed in appproxy.go
  • The gateway's apps.go will need a new EnrollApp proxy method (same pattern as existing RPCs) — but this is in agynio/gateway repo

Dependencies

  • Requires agynio/api issue #64 (proto change) to be merged and published to buf.build/agynio/api first
  • After proto is published: buf dep update + buf generate to regenerate

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions