23.2. Basic ACL 패키지

주의할 점은 현재 기본적인 ACL 서비스들이 리팩토링중에 있다는 것이다. 우리는 1.1.0 릴리즈에서 새로운 코드가 포함되기를 예상하고 있다. 계획된 코드는 이미 Acegi Security의 서브버전 샌드박스에 들어 있으므로 여러분이 ACL을 필요로 하는 새로운 애플리케이션을 개발해야 하거나 기획단계에 있다면 확인해 보길 바란다. Basic ACL 서비스는 1.1.0 릴리즈부터는 폐기될 것이다.

org.acegisecurity.acl 패키지는 매우 단순하며, 그림 6에 나타나 있는 것과 같이 단 하나의 유용한 인터페이스와 클래스만으로 구성되어 있다. 이 패키지는 접근 제어 목록(ACL) 탐색에 필요한 기본적인 요소들만을 제공하고 있다.

그림 6. 접근 제어 목록 관리자

중앙에 있는 인터페이스가 AclManager이며, 아래의 두 메소드들을 정의하고 있다:

public AclEntry[] getAcls(java.lang.Object domainInstance);
public AclEntry[] getAcls(java.lang.Object domainInstance, Authentication authentication);

AclManager는 여러분의 비즈니스 객체에 대한 협력자로서, 아니면 좀 더 바람직하게는 AccessDecisionVoter로서 사용될 목적으로 만들어진 것이다. 이는 여러분이 Spring의 일반적인 ApplicationContext 기능을 이용하여 여러분의 AccessDecisionVoter (혹은 비즈니스 메소드)와 AclManager를 묶는다는 것을 의미한다. ACL 정보를 ContextHolder에 넣는 것도 고려되었으나 이렇게 하는 것은 사용되지 않는 ACL 정보를 잠재적으로 로딩하는데 소모되는 시간 뿐만 아니라 메모리 사용 관점에서도 비효율적일 수 있다고 생각되었다. 그러한 ACL 정보를 필요로 하는 객체들과 협력자를 묶는 것의 필요성에 대한 트레이드 오프는 상대적으로 덜 중요하며, Spring이 관리하는 애플리케이션의 경우에는 특히 그러하다.

AclManager의 첫 번째 메소드는 전달된 도메인 객체에 적용되는 모든 ACL을 반환할 것이다. 두 번째 메소드 역시 동일하지만, 전달된 Authentication 객체에 적용되는 ACL만을 반환한다.

AclManager에 의해 반환되는 AclEntry 인터페이스는 단순히 마커 인터페이스일 뿐이다. 여러분은 애플리케이션에 대한 ACL 접근권한을 나타내는 구현체를 제공해야 할 것이다.

org.acegisecurity.acl 패키지를 마무리하는 것은 AclProviderManager 클래스이며, 이 클래스는 그것에 대응되는 AclProvider 인터페이스를 가진다. AclProviderManagerAclManager의 구상 구현체이며, 등록된 AclProvider들을 순회한다. 제시된 도메인 객체에 대한 ACL 정보를 신뢰할 수 있게 제공하는 데에는 첫 번째 AclProvider를 이용한다. 이는 인증에 사용되는 AuthenticationProvider 인터페이스와 매우 유사하다.

이러한 배경지식을 가지고 사용가능한 ACL 구현체들을 살펴보기로 하자.

Acegi Security는 제품 수준의 품질을 갖춘 ACL 제공자 구현체를 포함하고 있으며, 이러한 ACL 제공자 구현체들은 그림 7에 나타나 있다.

그림 7. Basic ACL 관리자

구현체는 정수 마스킹에 기반하고 있으며, 정수 마스킹은 ACL 접근 권한에 널리 사용되어 유연성과 속도를 부여한다. 유닉스의 chmod 명령어를 사용해본 사람들은 이러한 유형의 권한 마스킹(예를 들면 chmod 777)에 관해 잘 알고 있을 것이다. 여러분은 org.acegisecurity.acl.basic에서 정수 마스킹 ACL 패키지에 들어있는 클래스와 인터페이스들을 찾아볼 수 있을 것이다.

AclEntry 인터페이스를 확장한 것이 BasicAclEntry 인터페이스이며, 주요 메소드가 아래에 나타나 있다:

public AclObjectIdentity getAclObjectIdentity();
public AclObjectIdentity getAclObjectParentIdentity();
public int getMask();
public java.lang.Object getRecipient();

위에 나타나 있는 것과 같이, 각 BasicAclEntry는 네 개의 주요 프로퍼티들을 포함하고 있다. maskrecipient에게 허가된 접근권한을 나타내는 정수이다. aclObjectIdentity는 ACL이 적용되는 도메인 객체 인스턴스를 식별할 수 있으며, 그리고 aclObjectParentIdentity는 선택적으로 도메인 객체 인스턴스의 부모를 지정한다. 여러 개의 BasicAclEntry가 일반적으로 하나의 도메인 객체 인스턴스에 존재하며, 그리고 부모 식별성(identity) 프로퍼티에 의해 제시되므로 객체 계층구조상의 높은 곳에서 승인된 접근권한은 아래로 흘러 내려와 상속될 것이다(아니면 0에 의해 차단된다).

BasicAclEntry 구현체들은 일반적으로 편의 메소드들을 제공하고 있는데, 가령 isReadAllowed()는 애플리케이션이 자기 자신에 대해 비트 마스크 연산을 수행하는 것을 방지하도록 해준다. SimpleAclEntryAbstractBasicAclEntry는 이러한 비트 마스크 로직에 관해 더 많은 것들을 보여주며 제공해 준다.

AclObjectIdentity 자체는 마커 인터페이스일 뿐이므로 여러분은 여러분의 도메인 객체에 대한 구현체를 제공할 필요가 있다. 하지만 패키지에는 여러 요구사항들에 적합한 NamedEntityObjectIdentity 구현체가 포함되어 있다. NamedEntityObjectIdentity는 주어진 도메인 객체 인스턴스를 인스턴스의 클래스명과 인스턴스의 식별성을 이용하여 식별한다. NamedEntityObjectIdentity는 수동으로(생성자를 호출하거나 클래스명과 식별자 String을 제시하여) 생성할 수 있거나 아니면 getId() 메소드를 포함하고 있는 도메인 객체에 전달하여 생성할 수 있다.

실질적인 AclProvider 구현체는 BasicAclProvider라는 이름을 가진다. BasicAclProvider는 인증에 관련된 DaoAuthenticationProvder에서 사용되었던 것과 유사한 설계를 도입하였다. 구체적으로 말하자면 여러분이 제공자에 BasicAclDao를 정의하여 서로 다른 ACL 저장소 유형들에 플러거블한 방식으로 접근할 수 있다는 것이다. 게다가 BasicAclProvider는 플러거블한 캐시 제공자를 지원하기도 한다(Acegi Security에는 EH-CACHE를 앞세운 구현체를 포함하고 있다).

BasicAclDao 인터페이스를 구현하는 것은 매우 간단하다:

public BasicAclEntry[] getAcls(AclObjectIdentity aclObjectIdentity);

BasicAclDao 구현체는 제시된 AclObjectIdentity와 그것이 어떻게 스토리지 저장소에 맵핑되며, 관련 레코드를 찾으며, 그리고 적절한 BasicAclEntry 객체를 생성하여 그것들을 반환하는지에 관해 알아야할 필요가 있다.

Acegi Security는 JdbcDaoImpl이라는 이름을 가진 단 하나의 BasicAclDao 구현체를 포함하고 있다. 이름이 나타내고 있듯이 JdbcDaoImpl은 JDBC 데이터베이스로부터 ACL 정보에 접근한다. 또한 이러한 DAO를 확장한 버전인 JdbcExtendedDaoImpl도 존재하는데, JdbcExtendedDaoImpl은 JDBC 데이터베이스에 대한 CRUD 연산을 제공하고 있으며, 여기에서는 그러한 기능들에 대해서는 논의하지 않을 것이다. 기본 데이터베이스 스키마 및 몇 가지 예제 데이터들은 이 기능을 이해하는데 도움될 것이다:

CREATE TABLE acl_object_identity (
     id IDENTITY NOT NULL,
     object_identity VARCHAR_IGNORECASE(250) NOT NULL,
     parent_object INTEGER,
     acl_class VARCHAR_IGNORECASE(250) NOT NULL,
     CONSTRAINT unique_object_identity UNIQUE(object_identity),
     FOREIGN KEY (parent_object) REFERENCES acl_object_identity(id)
);

CREATE TABLE acl_permission (
     id IDENTITY NOT NULL,
     acl_object_identity INTEGER NOT NULL,
     recipient VARCHAR_IGNORECASE(100) NOT NULL,
     mask INTEGER NOT NULL,
     CONSTRAINT unique_recipient UNIQUE(acl_object_identity, recipient),
     FOREIGN KEY (acl_object_identity) REFERENCES acl_object_identity(id)
);

INSERT INTO acl_object_identity VALUES (1, 'corp.DomainObject:1', null, 'org.acegisecurity.acl.basic.SimpleAclEntry');
INSERT INTO acl_object_identity VALUES (2, 'corp.DomainObject:2', 1, 'org.acegisecurity.acl.basic.SimpleAclEntry');
INSERT INTO acl_object_identity VALUES (3, 'corp.DomainObject:3', 1, 'org.acegisecurity.acl.basic.SimpleAclEntry');
INSERT INTO acl_object_identity VALUES (4, 'corp.DomainObject:4', 1, 'org.acegisecurity.acl.basic.SimpleAclEntry');
INSERT INTO acl_object_identity VALUES (5, 'corp.DomainObject:5', 3, 'org.acegisecurity.acl.basic.SimpleAclEntry');
INSERT INTO acl_object_identity VALUES (6, 'corp.DomainObject:6', 3, 'org.acegisecurity.acl.basic.SimpleAclEntry');

INSERT INTO acl_permission VALUES (null, 1, 'ROLE_SUPERVISOR', 1);
INSERT INTO acl_permission VALUES (null, 2, 'ROLE_SUPERVISOR', 0);
INSERT INTO acl_permission VALUES (null, 2, 'marissa', 2);
INSERT INTO acl_permission VALUES (null, 3, 'scott', 14);
INSERT INTO acl_permission VALUES (null, 6, 'scott', 1);

여러분도 볼 수 있었겠지만, 데이터베이스에 특화되어 있는 제약들은 ACL 정보의 무결성을 보장하기 위해 폭넓게 사용된다. 만약 여러분이 다른 데이터베이스(위에는 Hypersonic SQL문이 나타나 있다)를 사용할 필요가 있다면, 여러분은 상응하는 제약들도 구현해 보아야만 할 것이다. 상응하는 오라클 설정은 다음과 같다:

CREATE TABLE ACL_OBJECT_IDENTITY (
     ID number(19,0) not null,
     OBJECT_IDENTITY varchar2(255) NOT NULL,
     PARENT_OBJECT number(19,0),
     ACL_CLASS varchar2(255) NOT NULL,
     primary key (ID)
);
ALTER TABLE ACL_OBJECT_IDENTITY ADD CONTRAINT FK_PARENT_OBJECT foreign key (ID) references ACL_OBJECT_IDENTITY

CREATE SEQUENCE ACL_OBJECT_IDENTITY_SEQ;

CREATE OR REPLACE TRIGGER ACL_OBJECT_IDENTITY_ID
BEFORE INSERT ON ACL_OBJECT_IDENTITY
FOR EACH ROW
BEGIN
  SELECT ACL_OBJECT_IDENTITY_SEQ.NEXTVAL INTO :new.id FROM dual;
END;

CREATE TABLE ACL_PERMISSION (
     ID number(19,0) not null,
     ACL_OBJECT_IDENTITY number(19,0) NOT NULL,
     RECIPIENT varchar2(255) NOT NULL,
     MASK number(19,0) NOT NULL,
     primary key (ID)
);

ALTER TABLE ACL_PERMISSION ADD CONTRAINT UNIQUE_ID_RECIPIENT unique (acl_object_identity, recipient);

CREATE SEQUENCE ACL_PERMISSION_SEQ;

CREATE OR REPLACE TRIGGER ACL_PERMISSION_ID
BEFORE INSERT ON ACL_PERMISSION
FOR EACH ROW
BEGIN
  SELECT ACL_PERMISSION_SEQ.NEXTVAL INTO :new.id FROM dual;
END;

<bean id="basicAclExtendedDao" class="org.acegisecurity.acl.basic.jdbc.JdbcExtendedDaoImpl">
    <property name="dataSource">
        <ref bean="dataSource"/>
    </property>
    <property name="objectPropertiesQuery" value="${acegi.objectPropertiesQuery}"/>
</bean>

<prop key="acegi.objectPropertiesQuery">SELECT CHILD.ID, CHILD.OBJECT_IDENTITY, CHILD.ACL_CLASS, PARENT.OBJECT_IDENTITY as PARENT_OBJECT_IDENTITY FROM acl_object_identity as CHILD LEFT OUTER JOIN acl_object_identity as PARENT ON CHILD.parent_object=PARENT.id WHERE CHILD.object_identity = ?</prop> 

JdbcDaoImpl은 오직 NamedEntityObjectIdentity에 대한 요청에만 응답할 것이다. JdbcDaoImpl은 그러한 식별성들을 하나의 문자열로 변환하며, 문자열은 NamedEntityObjectIdentity.getClassname() + ":" + NamedEntityObjectIdentity.getId()로 이루어진다. 이는 위에서 본 object_identity 유형의 값을 만들어 낸다. 예제 데이터가 나타내는 것과 같이 각각의 데이터베이스 행은 하나의 BasicAclEntry에 대응된다. 앞서 언급했으며 위 예제 데이터의 corp.DomainObject:2로 보여준 것과 같이 각각의 도메인 객체 인스턴스는 종종 여러개의 BasicAclEntry를 포함하게 될 것이다.

JdbcDaoImpl은 구상 BasicAclEntry 클래스를 반환해야 할 필요가 있으므로 어느 BasicAclEntry 구현체가 생성되어 저장되어야 할지 알고 있을 필요가 있다. 이는 acl_class 컬럼의 역할이다. JdbcDaoImpl은 지정된 클래스를 생성하여 mask, recipient, aclObjectIdentityaclObjectParentIdentity 프로퍼티를 생성할 것이다.

아마 여러분이 동일한 데이터를 분간해 낼 수 있듯이, parent_object_identity 값은 null이나 object_identity와 동일한 형식을 취할 수 있다. 만약 null이 아니면 JdbcDaoImplNamedEntityObjectIdentity를 생성하여 반환된 BasicAclEntry 클래스내에 위치시킬 것이다.

BasicAclProvider로 돌아가서 BasicAclProviderBasicAclDao 구현체를 폴링할 수 있기 전에 BasicAclProvider는 전달된 도메인 객체 인스턴스를 AclObjectIdentity로 변환할 필요가 있다. BasicAclProvider는 이를 책임지고 있는 protected AclObjectIdentity obtainIdentity(Object domainInstance) 메소드를 포함하고 있다. obtainIdentity(Object) 메소드는 protected 메소드이므로 서브클래싱을 통해 쉽게 재정의될 수 있다. 일반적인 구현체은 전달된 도메인 객체 인스턴스가 AclObjectIdentityAware 인터페이스를 구현하고 있는지 여부를 확인하는데, AclObjectIdentityAware는 단지 AclObjectIdentity에 대한 접근자일 뿐이다. 만약 도메인 객체가 이 인터페이스를 구현하면 메소드는 도메인 객체 인스턴스를 BasicAclProvider.getDefaultAclObjectIdentity() 메소드에 의해 정의되어 있는 클래스의 생성자로 전달하여 AclObjectIdentity를 생성하려 할 것이다. 기본적으로 정의되어 있는 클래스는 NamedEntityObjectIdentity이며, 이 클래스는 위에서 좀 더 상세하게 설명되어 있다. 따라서 여러분은 (i) 여러분의 도메인 객체에 getId() 메소드를 제공하거나, (ii) 여러분의 도메인 객체에 AclObjectIdentityAware를 구현하거나, (iii) 생성자에 여러분의 도메인 객체를 받아들이는 대안 AclObjectIdentity 구현체를 제공하거나, (iv) obtainIdentity(Object) 메소드를 재정의해야 할 것이다.

일단 도메인 객체 인스턴스의 AclObjectIdentity가 결정되고 나면 BasicAclProvider는 DAO를 폴링하여 그것의 BasicAclEntry를 획득할 것이다. 만약 DAO에 의해 반환된 것들 중에서 부모가 있음을 나타내는 것이 있다면 그러한 부모가 폴링될 것이며, 그리고 그러한 과정은 더 이상 부모가 없을 때까지 반복될 것이다. 도메인 객체 인스턴스에 가장 가까운 recipient에 할당된 접근권한은 항상 우선순위를 취할 것이며 상속된 접근권한들을 재정의할 것이다. 위의 예제 데이터에서는 다음의 상속된 접근권한들이 적용될 것이다:

--- Mask integer 0  = no permissions
--- Mask integer 1  = administer
--- Mask integer 2  = read
--- Mask integer 6  = read and write permissions
--- Mask integer 14 = read and write and create permissions

---------------------------------------------------------------------
--- *** INHERITED RIGHTS FOR DIFFERENT INSTANCES AND RECIPIENTS ***
--- INSTANCE  RECIPIENT         PERMISSION(S) (COMMENT #INSTANCE)
---------------------------------------------------------------------
---    1      ROLE_SUPERVISOR   Administer
---    2      ROLE_SUPERVISOR   None (overrides parent #1)
---           marissa           Read
---    3      ROLE_SUPERVISOR   Administer (from parent #1)
---           scott             Read, Write, Create
---    4      ROLE_SUPERVISOR   Administer (from parent #1)
---    5      ROLE_SUPERVISOR   Administer (from parent #3)
---           scott             Read, Write, Create (from parent #3)
---    6      ROLE_SUPERVISOR   Administer (from parent #3)
---           scott             Administer (overrides parent #3)

따라서 위 내용은 도메인 객체 인스턴스가 어떻게 AclObjectIdentity를 발견했는지를 설명해 주며, BasicAclDao는 상속된 접근권한의 배열이 도메인 객체 인스턴스에 대하여 생성될 때까지 연속적으로 폴링될 것이다. 마지막 단계는 BasicAclEntry[]가 실제로 주어진 Authentication 객체에 적용가능한지를 판단하는 것이다.

여러분도 상기했겠지만, AclManager(그리고 모든 위임하는, BasicAclProvider에 의존하고 포함하는)는 전달된 Authentication 객체에 적용되는 BasicAclEntry[] 만을 반환하는 메소드를 제공한다. BasicAclProviderEffectiveAclsResolver 구현체에 필터링 연산을 위임하여 이러한 기능을 제공해 준다. 기본 구현체인 GrantedAuthorityEffectiveAclsResolverBasicAclEntry[]를 순회하여 recipientAuthentication의 인증주체나 AuthenticationGrantedAuthority[]와 동일한 것만을 포함시킨다. 좀 더 자세한 내용은 JavaDoc을 참조하길 바란다.

그림 8. ACL 인스턴스화 접근법

위 그림은 Basic ACL 패키지에 들어있는 객체간의 주요 관계를 설명하고 있다.