Skip to content

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

@rowan-stein

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions